From 259b48e08263a9d363b6d7d589489ccb2dbeab8a Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Fri, 25 Oct 2019 14:12:58 -0700 Subject: [PATCH] Project import generated by Copybara. GitOrigin-RevId: b137378673f7d66d41bcd46e4fc3a0d9ef254894 --- README.md | 7 +- WORKSPACE | 3 +- mediapipe/calculators/core/BUILD | 67 + .../core/dequantize_byte_array_calculator.cc | 90 + .../dequantize_byte_array_calculator.proto | 28 + .../dequantize_byte_array_calculator_test.cc | 137 + .../core/previous_loopback_calculator.cc | 6 + .../core/previous_loopback_calculator_test.cc | 91 + .../core/side_packet_to_stream_calculator.cc | 83 + .../core/split_vector_calculator.h | 101 +- .../core/split_vector_calculator.proto | 3 + .../core/split_vector_calculator_test.cc | 134 + .../core/string_to_int_calculator.cc | 79 + mediapipe/calculators/tensorflow/BUILD | 80 + .../lapped_tensor_buffer_calculator.cc | 61 +- .../tensorflow/tfrecord_reader_calculator.cc | 126 + ...unpack_yt8m_sequence_example_calculator.cc | 192 + .../vector_float_to_tensor_calculator.cc | 6 +- .../vector_int_to_tensor_calculator.cc | 203 + ...tor_int_to_tensor_calculator_options.proto | 43 + .../vector_int_to_tensor_calculator_test.cc | 202 + .../tflite/tflite_converter_calculator.cc | 35 +- .../tflite/tflite_inference_calculator.cc | 54 +- ...te_tensors_to_classification_calculator.cc | 26 +- ...tflite_tensors_to_detections_calculator.cc | 47 +- .../tflite_tensors_to_landmarks_calculator.cc | 6 +- ...lite_tensors_to_segmentation_calculator.cc | 39 +- mediapipe/calculators/util/BUILD | 101 +- .../util/annotation_overlay_calculator.cc | 46 +- .../detection_label_id_to_text_calculator.cc | 4 +- .../util/detections_to_rects_calculator.cc | 95 +- .../util/detections_to_rects_calculator.h | 105 + .../util/labels_to_render_data_calculator.cc | 181 + .../labels_to_render_data_calculator.proto | 62 + .../util/landmarks_to_floats_calculator.cc | 138 + .../util/landmarks_to_floats_calculator.proto | 28 + .../util/local_file_contents_calculator.cc | 57 + .../util/top_k_scores_calculator.cc | 50 +- mediapipe/docs/android_archive_library.md | 130 + mediapipe/docs/examples.md | 5 +- mediapipe/docs/face_detection_desktop.md | 6 +- mediapipe/docs/hair_segmentation_desktop.md | 3 +- mediapipe/docs/hand_tracking_desktop.md | 6 +- mediapipe/docs/images/mobile/aar_location.png | Bin 0 -> 35658 bytes .../mobile/android_studio_opencv_location.png | Bin 0 -> 76954 bytes .../docs/images/mobile/assets_location.png | Bin 0 -> 57650 bytes mediapipe/docs/install.md | 72 +- mediapipe/docs/object_detection_desktop.md | 9 +- mediapipe/docs/youtube_8m.md | 101 +- mediapipe/examples/desktop/BUILD | 2 + .../examples/desktop/simple_run_graph_main.cc | 92 +- mediapipe/examples/desktop/youtube8m/BUILD | 11 + .../examples/desktop/youtube8m/README.md | 104 +- .../desktop/youtube8m/viewer/server.py | 262 ++ .../youtube8m/viewer/static/index.html | 96 + .../desktop/youtube8m/viewer/static/main.js | 217 + mediapipe/framework/BUILD | 13 + .../framework/calculator_graph_bounds_test.cc | 90 +- .../calculator_graph_side_packet_test.cc | 61 + mediapipe/framework/calculator_node.cc | 47 +- mediapipe/framework/calculator_node.h | 3 + mediapipe/framework/collection.h | 16 + mediapipe/framework/demangle.h | 14 +- mediapipe/framework/deps/BUILD | 5 +- .../framework/formats/image_format.proto | 4 + mediapipe/framework/formats/image_frame.cc | 6 + .../framework/formats/image_frame_opencv.cc | 3 + mediapipe/framework/formats/landmark.proto | 5 + .../framework/input_side_packet_handler.cc | 7 + .../framework/input_side_packet_handler.h | 5 + .../framework/output_side_packet_impl.cc | 5 +- mediapipe/framework/output_side_packet_impl.h | 1 + mediapipe/framework/packet.h | 8 + mediapipe/framework/port.h | 18 + .../framework/profiler/graph_profiler_test.cc | 246 +- mediapipe/framework/profiler/trace_buffer.h | 17 +- mediapipe/framework/profiler/trace_builder.cc | 32 +- mediapipe/framework/tool/tag_map.h | 5 + mediapipe/framework/tool/template_expander.cc | 4 + mediapipe/framework/tool/template_parser.cc | 4 +- mediapipe/gpu/gl_simple_shaders.h | 4 + mediapipe/graphs/youtube8m/BUILD | 27 + mediapipe/graphs/youtube8m/label_map.txt | 3862 +++++++++++++++++ .../local_video_model_inference.pbtxt | 178 + .../yt8m_dataset_model_inference.pbtxt | 139 + mediapipe/java/com/google/mediapipe/BUILD | 15 + .../com/google/mediapipe/components/BUILD | 7 + .../components/ExternalTextureConverter.java | 4 +- .../java/com/google/mediapipe/framework/BUILD | 7 + .../java/com/google/mediapipe/glutil/BUILD | 7 + .../com/google/mediapipe/mediapipe_aar.bzl | 157 + mediapipe/util/sequence/README.md | 2 + mediapipe/util/sequence/media_sequence.py | 4 +- .../util/sequence/media_sequence_test.py | 4 + 94 files changed, 8567 insertions(+), 401 deletions(-) create mode 100644 mediapipe/calculators/core/dequantize_byte_array_calculator.cc create mode 100644 mediapipe/calculators/core/dequantize_byte_array_calculator.proto create mode 100644 mediapipe/calculators/core/dequantize_byte_array_calculator_test.cc create mode 100644 mediapipe/calculators/core/side_packet_to_stream_calculator.cc create mode 100644 mediapipe/calculators/core/string_to_int_calculator.cc create mode 100644 mediapipe/calculators/tensorflow/tfrecord_reader_calculator.cc create mode 100644 mediapipe/calculators/tensorflow/unpack_yt8m_sequence_example_calculator.cc create mode 100644 mediapipe/calculators/tensorflow/vector_int_to_tensor_calculator.cc create mode 100644 mediapipe/calculators/tensorflow/vector_int_to_tensor_calculator_options.proto create mode 100644 mediapipe/calculators/tensorflow/vector_int_to_tensor_calculator_test.cc create mode 100644 mediapipe/calculators/util/detections_to_rects_calculator.h create mode 100644 mediapipe/calculators/util/labels_to_render_data_calculator.cc create mode 100644 mediapipe/calculators/util/labels_to_render_data_calculator.proto create mode 100644 mediapipe/calculators/util/landmarks_to_floats_calculator.cc create mode 100644 mediapipe/calculators/util/landmarks_to_floats_calculator.proto create mode 100644 mediapipe/calculators/util/local_file_contents_calculator.cc create mode 100644 mediapipe/docs/android_archive_library.md create mode 100644 mediapipe/docs/images/mobile/aar_location.png create mode 100644 mediapipe/docs/images/mobile/android_studio_opencv_location.png create mode 100644 mediapipe/docs/images/mobile/assets_location.png create mode 100644 mediapipe/examples/desktop/youtube8m/viewer/server.py create mode 100644 mediapipe/examples/desktop/youtube8m/viewer/static/index.html create mode 100644 mediapipe/examples/desktop/youtube8m/viewer/static/main.js create mode 100644 mediapipe/graphs/youtube8m/label_map.txt create mode 100644 mediapipe/graphs/youtube8m/local_video_model_inference.pbtxt create mode 100644 mediapipe/graphs/youtube8m/yt8m_dataset_model_inference.pbtxt create mode 100644 mediapipe/java/com/google/mediapipe/BUILD create mode 100644 mediapipe/java/com/google/mediapipe/mediapipe_aar.bzl diff --git a/README.md b/README.md index 97d727466..56cd7bac0 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,15 @@ A web-based visualizer is hosted on [viz.mediapipe.dev](https://viz.mediapipe.de * [Discuss](https://groups.google.com/forum/#!forum/mediapipe) - General community discussion around MediaPipe ## Publications +* [On-Device, Real-Time Hand Tracking with MediaPipe](https://ai.googleblog.com/2019/08/on-device-real-time-hand-tracking-with.html) * [MediaPipe: A Framework for Building Perception Pipelines](https://arxiv.org/abs/1906.08172) ## Events -[Open sourced at CVPR 2019](https://sites.google.com/corp/view/perception-cv4arvr/mediapipe) on June 17~20 in Long Beach, CA +* [ML Conference, Berlin 9-11 Dec 2019](https://mlconference.ai/machine-learning-advanced-development/mediapipe-building-real-time-cross-platform-mobile-web-edge-desktop-video-audio-ml-pipelines/) +* [The 3rd Workshop on YouTube-8M Large Scale Video Understanding Workshop](https://research.google.com/youtube8m/workshop2019/index.html) Seoul, Korea ICCV 2019 +* [AI DevWorld 2019](https://aidevworld.com) on Oct 10 in San Jose, California +* [Google Industry Workshop at ICIP 2019](http://2019.ieeeicip.org/?action=page4&id=14#Google) [Presentation](https://docs.google.com/presentation/d/e/2PACX-1vRIBBbO_LO9v2YmvbHHEt1cwyqH6EjDxiILjuT0foXy1E7g6uyh4CesB2DkkEwlRDO9_lWfuKMZx98T/pub?start=false&loop=false&delayms=3000&slide=id.g556cc1a659_0_5) on Sept 24 in Taipei, Taiwan +* [Open sourced at CVPR 2019](https://sites.google.com/corp/view/perception-cv4arvr/mediapipe) on June 17~20 in Long Beach, CA ## Alpha Disclaimer MediaPipe is currently in alpha for v0.6. We are still making breaking API changes and expect to get to stable API by v1.0. diff --git a/WORKSPACE b/WORKSPACE index 0aee35c67..f568e8e99 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -10,7 +10,8 @@ http_archive( sha256 = "2ef429f5d7ce7111263289644d233707dba35e39696377ebab8b0bc701f7818e", ) load("@bazel_skylib//lib:versions.bzl", "versions") -versions.check(minimum_bazel_version = "0.24.1") +versions.check(minimum_bazel_version = "0.24.1", + maximum_bazel_version = "0.29.1") # ABSL cpp library. http_archive( diff --git a/mediapipe/calculators/core/BUILD b/mediapipe/calculators/core/BUILD index 80205f90e..4abb4c4b2 100644 --- a/mediapipe/calculators/core/BUILD +++ b/mediapipe/calculators/core/BUILD @@ -26,6 +26,13 @@ proto_library( deps = ["//mediapipe/framework:calculator_proto"], ) +proto_library( + name = "dequantize_byte_array_calculator_proto", + srcs = ["dequantize_byte_array_calculator.proto"], + visibility = ["//visibility:public"], + deps = ["//mediapipe/framework:calculator_proto"], +) + proto_library( name = "packet_cloner_calculator_proto", srcs = ["packet_cloner_calculator.proto"], @@ -104,6 +111,14 @@ mediapipe_cc_proto_library( deps = [":concatenate_vector_calculator_proto"], ) +mediapipe_cc_proto_library( + name = "dequantize_byte_array_calculator_cc_proto", + srcs = ["dequantize_byte_array_calculator.proto"], + cc_deps = ["//mediapipe/framework:calculator_cc_proto"], + visibility = ["//visibility:public"], + deps = [":dequantize_byte_array_calculator_proto"], +) + mediapipe_cc_proto_library( name = "quantize_float_vector_calculator_cc_proto", srcs = ["quantize_float_vector_calculator.proto"], @@ -387,6 +402,32 @@ cc_library( alwayslink = 1, ) +cc_library( + name = "string_to_int_calculator", + srcs = ["string_to_int_calculator.cc"], + visibility = ["//visibility:public"], + deps = [ + "//mediapipe/framework:calculator_framework", + "//mediapipe/framework/port:integral_types", + "//mediapipe/framework/port:status", + "@com_google_absl//absl/strings", + ], + alwayslink = 1, +) + +cc_library( + name = "side_packet_to_stream_calculator", + srcs = ["side_packet_to_stream_calculator.cc"], + visibility = ["//visibility:public"], + deps = [ + "//mediapipe/framework:calculator_framework", + "//mediapipe/framework/port:logging", + "//mediapipe/framework/port:ret_check", + "//mediapipe/framework/port:status", + ], + alwayslink = 1, +) + cc_test( name = "immediate_mux_calculator_test", srcs = ["immediate_mux_calculator_test.cc"], @@ -558,6 +599,32 @@ cc_test( ], ) +cc_library( + name = "dequantize_byte_array_calculator", + srcs = ["dequantize_byte_array_calculator.cc"], + visibility = ["//visibility:public"], + deps = [ + ":dequantize_byte_array_calculator_cc_proto", + "//mediapipe/framework:calculator_context", + "//mediapipe/framework:calculator_framework", + "//mediapipe/framework/port:status", + ], + alwayslink = 1, +) + +cc_test( + name = "dequantize_byte_array_calculator_test", + srcs = ["dequantize_byte_array_calculator_test.cc"], + deps = [ + ":dequantize_byte_array_calculator", + "//mediapipe/framework:calculator_framework", + "//mediapipe/framework:calculator_runner", + "//mediapipe/framework/port:gtest_main", + "//mediapipe/framework/port:parse_text_proto", + "//mediapipe/framework/port:status", + ], +) + cc_library( name = "quantize_float_vector_calculator", srcs = ["quantize_float_vector_calculator.cc"], diff --git a/mediapipe/calculators/core/dequantize_byte_array_calculator.cc b/mediapipe/calculators/core/dequantize_byte_array_calculator.cc new file mode 100644 index 000000000..4f1a3ed86 --- /dev/null +++ b/mediapipe/calculators/core/dequantize_byte_array_calculator.cc @@ -0,0 +1,90 @@ +// Copyright 2019 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 "mediapipe/calculators/core/dequantize_byte_array_calculator.pb.h" +#include "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/port/canonical_errors.h" +#include "mediapipe/framework/port/status.h" + +// Dequantizes a byte array to a vector of floats. +// +// Example config: +// node { +// calculator: "DequantizeByteArrayCalculator" +// input_stream: "ENCODED:encoded" +// output_stream: "FLOAT_VECTOR:float_vector" +// options { +// [mediapipe.DequantizeByteArrayCalculatorOptions.ext]: { +// max_quantized_value: 2 +// min_quantized_value: -2 +// } +// } +// } +namespace mediapipe { + +class DequantizeByteArrayCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->Inputs().Tag("ENCODED").Set(); + cc->Outputs().Tag("FLOAT_VECTOR").Set>(); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Open(CalculatorContext* cc) final { + const auto options = + cc->Options<::mediapipe::DequantizeByteArrayCalculatorOptions>(); + if (!options.has_max_quantized_value() || + !options.has_min_quantized_value()) { + return ::mediapipe::InvalidArgumentError( + "Both max_quantized_value and min_quantized_value must be provided " + "in DequantizeByteArrayCalculatorOptions."); + } + float max_quantized_value = options.max_quantized_value(); + float min_quantized_value = options.min_quantized_value(); + if (max_quantized_value < min_quantized_value + FLT_EPSILON) { + return ::mediapipe::InvalidArgumentError( + "max_quantized_value must be greater than min_quantized_value."); + } + float range = max_quantized_value - min_quantized_value; + scalar_ = range / 255.0; + bias_ = (range / 512.0) + min_quantized_value; + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) final { + const std::string& encoded = + cc->Inputs().Tag("ENCODED").Value().Get(); + std::vector float_vector; + float_vector.reserve(encoded.length()); + for (int i = 0; i < encoded.length(); ++i) { + float_vector.push_back( + static_cast(encoded.at(i)) * scalar_ + bias_); + } + cc->Outputs() + .Tag("FLOAT_VECTOR") + .AddPacket(MakePacket>(float_vector) + .At(cc->InputTimestamp())); + return ::mediapipe::OkStatus(); + } + + private: + float scalar_; + float bias_; +}; + +REGISTER_CALCULATOR(DequantizeByteArrayCalculator); + +} // namespace mediapipe diff --git a/mediapipe/calculators/core/dequantize_byte_array_calculator.proto b/mediapipe/calculators/core/dequantize_byte_array_calculator.proto new file mode 100644 index 000000000..3032dbf48 --- /dev/null +++ b/mediapipe/calculators/core/dequantize_byte_array_calculator.proto @@ -0,0 +1,28 @@ +// Copyright 2019 The MediaPipe Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package mediapipe; + +import "mediapipe/framework/calculator.proto"; + +message DequantizeByteArrayCalculatorOptions { + extend CalculatorOptions { + optional DequantizeByteArrayCalculatorOptions ext = 272316343; + } + + optional float max_quantized_value = 1; + optional float min_quantized_value = 2; +} diff --git a/mediapipe/calculators/core/dequantize_byte_array_calculator_test.cc b/mediapipe/calculators/core/dequantize_byte_array_calculator_test.cc new file mode 100644 index 000000000..a17fb6281 --- /dev/null +++ b/mediapipe/calculators/core/dequantize_byte_array_calculator_test.cc @@ -0,0 +1,137 @@ +// Copyright 2019 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 "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/calculator_runner.h" +#include "mediapipe/framework/port/gmock.h" +#include "mediapipe/framework/port/gtest.h" +#include "mediapipe/framework/port/parse_text_proto.h" +#include "mediapipe/framework/port/status.h" +#include "mediapipe/framework/port/status_matchers.h" // NOLINT + +namespace mediapipe { + +TEST(QuantizeFloatVectorCalculatorTest, WrongConfig) { + CalculatorGraphConfig::Node node_config = + ParseTextProtoOrDie(R"( + calculator: "DequantizeByteArrayCalculator" + input_stream: "ENCODED:encoded" + output_stream: "FLOAT_VECTOR:float_vector" + options { + [mediapipe.DequantizeByteArrayCalculatorOptions.ext]: { + max_quantized_value: 2 + } + } + )"); + CalculatorRunner runner(node_config); + std::string empty_string; + runner.MutableInputs()->Tag("ENCODED").packets.push_back( + MakePacket(empty_string).At(Timestamp(0))); + auto status = runner.Run(); + EXPECT_FALSE(status.ok()); + EXPECT_THAT( + status.message(), + testing::HasSubstr( + "Both max_quantized_value and min_quantized_value must be provided")); +} + +TEST(QuantizeFloatVectorCalculatorTest, WrongConfig2) { + CalculatorGraphConfig::Node node_config = + ParseTextProtoOrDie(R"( + calculator: "DequantizeByteArrayCalculator" + input_stream: "ENCODED:encoded" + output_stream: "FLOAT_VECTOR:float_vector" + options { + [mediapipe.DequantizeByteArrayCalculatorOptions.ext]: { + max_quantized_value: -2 + min_quantized_value: 2 + } + } + )"); + CalculatorRunner runner(node_config); + std::string empty_string; + runner.MutableInputs()->Tag("ENCODED").packets.push_back( + MakePacket(empty_string).At(Timestamp(0))); + auto status = runner.Run(); + EXPECT_FALSE(status.ok()); + EXPECT_THAT( + status.message(), + testing::HasSubstr( + "max_quantized_value must be greater than min_quantized_value")); +} + +TEST(QuantizeFloatVectorCalculatorTest, WrongConfig3) { + CalculatorGraphConfig::Node node_config = + ParseTextProtoOrDie(R"( + calculator: "DequantizeByteArrayCalculator" + input_stream: "ENCODED:encoded" + output_stream: "FLOAT_VECTOR:float_vector" + options { + [mediapipe.DequantizeByteArrayCalculatorOptions.ext]: { + max_quantized_value: 1 + min_quantized_value: 1 + } + } + )"); + CalculatorRunner runner(node_config); + std::string empty_string; + runner.MutableInputs()->Tag("ENCODED").packets.push_back( + MakePacket(empty_string).At(Timestamp(0))); + auto status = runner.Run(); + EXPECT_FALSE(status.ok()); + EXPECT_THAT( + status.message(), + testing::HasSubstr( + "max_quantized_value must be greater than min_quantized_value")); +} + +TEST(DequantizeByteArrayCalculatorTest, TestDequantization) { + CalculatorGraphConfig::Node node_config = + ParseTextProtoOrDie(R"( + calculator: "DequantizeByteArrayCalculator" + input_stream: "ENCODED:encoded" + output_stream: "FLOAT_VECTOR:float_vector" + options { + [mediapipe.DequantizeByteArrayCalculatorOptions.ext]: { + max_quantized_value: 2 + min_quantized_value: -2 + } + } + )"); + CalculatorRunner runner(node_config); + unsigned char input[4] = {0x7F, 0xFF, 0x00, 0x01}; + runner.MutableInputs()->Tag("ENCODED").packets.push_back( + MakePacket( + std::string(reinterpret_cast(input), 4)) + .At(Timestamp(0))); + auto status = runner.Run(); + MP_ASSERT_OK(runner.Run()); + const std::vector& outputs = + runner.Outputs().Tag("FLOAT_VECTOR").packets; + EXPECT_EQ(1, outputs.size()); + const std::vector& result = outputs[0].Get>(); + ASSERT_FALSE(result.empty()); + EXPECT_EQ(4, result.size()); + EXPECT_NEAR(0, result[0], 0.01); + EXPECT_NEAR(2, result[1], 0.01); + EXPECT_NEAR(-2, result[2], 0.01); + EXPECT_NEAR(-1.976, result[3], 0.01); + + EXPECT_EQ(Timestamp(0), outputs[0].Timestamp()); +} + +} // namespace mediapipe diff --git a/mediapipe/calculators/core/previous_loopback_calculator.cc b/mediapipe/calculators/core/previous_loopback_calculator.cc index 6b23a0e70..8c470ef7d 100644 --- a/mediapipe/calculators/core/previous_loopback_calculator.cc +++ b/mediapipe/calculators/core/previous_loopback_calculator.cc @@ -102,6 +102,12 @@ class PreviousLoopbackCalculator : public CalculatorBase { cc->Outputs().Get(loop_out_id_).AddPacket(std::move(previous_loopback)); } } + if (!main_ts_.empty()) { + cc->Outputs().Get(loop_out_id_).SetNextTimestampBound(main_ts_.front()); + } + if (cc->Inputs().Get(main_id_).IsDone() && main_ts_.empty()) { + cc->Outputs().Get(loop_out_id_).Close(); + } return ::mediapipe::OkStatus(); } diff --git a/mediapipe/calculators/core/previous_loopback_calculator_test.cc b/mediapipe/calculators/core/previous_loopback_calculator_test.cc index 6ad569865..4ac38e9f0 100644 --- a/mediapipe/calculators/core/previous_loopback_calculator_test.cc +++ b/mediapipe/calculators/core/previous_loopback_calculator_test.cc @@ -107,5 +107,96 @@ TEST(PreviousLoopbackCalculator, CorrectTimestamps) { MP_EXPECT_OK(graph_.WaitUntilDone()); } +// A Calculator that outputs a summary packet in CalculatorBase::Close(). +class PacketOnCloseCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->Inputs().Index(0).Set(); + cc->Outputs().Index(0).Set(); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Open(CalculatorContext* cc) final { + cc->SetOffset(TimestampDiff(0)); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) final { + sum_ += cc->Inputs().Index(0).Value().Get(); + cc->Outputs().Index(0).AddPacket(cc->Inputs().Index(0).Value()); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Close(CalculatorContext* cc) final { + cc->Outputs().Index(0).AddPacket( + MakePacket(sum_).At(Timestamp::Max())); + return ::mediapipe::OkStatus(); + } + + private: + int sum_ = 0; +}; +REGISTER_CALCULATOR(PacketOnCloseCalculator); + +// Demonstrates that all ouput and input streams in PreviousLoopbackCalculator +// will close as expected when all graph input streams are closed. +TEST(PreviousLoopbackCalculator, ClosesCorrectly) { + std::vector outputs; + CalculatorGraphConfig graph_config_ = + ParseTextProtoOrDie(R"( + input_stream: 'in' + node { + calculator: 'PreviousLoopbackCalculator' + input_stream: 'MAIN:in' + input_stream: 'LOOP:out' + input_stream_info: { tag_index: 'LOOP' back_edge: true } + output_stream: 'PREV_LOOP:previous' + } + # This calculator synchronizes its inputs as normal, so it is used + # to check that both "in" and "previous" are ready. + node { + calculator: 'PassThroughCalculator' + input_stream: 'in' + input_stream: 'previous' + output_stream: 'out' + output_stream: 'previous2' + } + node { + calculator: 'PacketOnCloseCalculator' + input_stream: 'out' + output_stream: 'close_out' + } + )"); + tool::AddVectorSink("close_out", &graph_config_, &outputs); + + CalculatorGraph graph_; + MP_ASSERT_OK(graph_.Initialize(graph_config_, {})); + MP_ASSERT_OK(graph_.StartRun({})); + + auto send_packet = [&graph_](const std::string& input_name, int n) { + MP_EXPECT_OK(graph_.AddPacketToInputStream( + input_name, MakePacket(n).At(Timestamp(n)))); + }; + + send_packet("in", 1); + MP_EXPECT_OK(graph_.WaitUntilIdle()); + EXPECT_EQ(TimestampValues(outputs), (std::vector{1})); + + send_packet("in", 5); + MP_EXPECT_OK(graph_.WaitUntilIdle()); + EXPECT_EQ(TimestampValues(outputs), (std::vector{1, 5})); + + send_packet("in", 15); + MP_EXPECT_OK(graph_.WaitUntilIdle()); + EXPECT_EQ(TimestampValues(outputs), (std::vector{1, 5, 15})); + + MP_EXPECT_OK(graph_.CloseAllInputStreams()); + MP_EXPECT_OK(graph_.WaitUntilIdle()); + EXPECT_EQ(TimestampValues(outputs), + (std::vector{1, 5, 15, Timestamp::Max().Value()})); + + MP_EXPECT_OK(graph_.WaitUntilDone()); +} + } // anonymous namespace } // namespace mediapipe diff --git a/mediapipe/calculators/core/side_packet_to_stream_calculator.cc b/mediapipe/calculators/core/side_packet_to_stream_calculator.cc new file mode 100644 index 000000000..043c91f32 --- /dev/null +++ b/mediapipe/calculators/core/side_packet_to_stream_calculator.cc @@ -0,0 +1,83 @@ +// Copyright 2019 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 "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/port/logging.h" +#include "mediapipe/framework/port/ret_check.h" +#include "mediapipe/framework/port/status.h" + +namespace mediapipe { + +using mediapipe::PacketTypeSet; +using mediapipe::Timestamp; + +namespace { +static std::map* kTimestampMap = []() { + auto* res = new std::map(); + res->emplace("AT_PRESTREAM", Timestamp::PreStream()); + res->emplace("AT_POSTSTREAM", Timestamp::PostStream()); + res->emplace("AT_ZERO", Timestamp(0)); + return res; +}(); + +} // namespace + +// Outputs the single input_side_packet at the timestamp specified in the +// output_stream tag. Valid tags are AT_PRESTREAM, AT_POSTSTREAM and AT_ZERO. +class SidePacketToStreamCalculator : public CalculatorBase { + public: + SidePacketToStreamCalculator() = default; + ~SidePacketToStreamCalculator() override = default; + + static ::mediapipe::Status GetContract(CalculatorContract* cc); + ::mediapipe::Status Process(CalculatorContext* cc) override; + ::mediapipe::Status Close(CalculatorContext* cc) override; +}; +REGISTER_CALCULATOR(SidePacketToStreamCalculator); + +::mediapipe::Status SidePacketToStreamCalculator::GetContract( + CalculatorContract* cc) { + cc->InputSidePackets().Index(0).SetAny(); + + std::set tags = cc->Outputs().GetTags(); + RET_CHECK_EQ(tags.size(), 1); + + RET_CHECK_EQ(kTimestampMap->count(*tags.begin()), 1); + cc->Outputs().Tag(*tags.begin()).SetAny(); + + return ::mediapipe::OkStatus(); +} + +::mediapipe::Status SidePacketToStreamCalculator::Process( + CalculatorContext* cc) { + return mediapipe::tool::StatusStop(); +} + +::mediapipe::Status SidePacketToStreamCalculator::Close(CalculatorContext* cc) { + std::set tags = cc->Outputs().GetTags(); + RET_CHECK_EQ(tags.size(), 1); + const std::string& tag = *tags.begin(); + RET_CHECK_EQ(kTimestampMap->count(tag), 1); + cc->Outputs().Tag(tag).AddPacket( + cc->InputSidePackets().Index(0).At(kTimestampMap->at(tag))); + + return ::mediapipe::OkStatus(); +} + +} // namespace mediapipe diff --git a/mediapipe/calculators/core/split_vector_calculator.h b/mediapipe/calculators/core/split_vector_calculator.h index def156474..0eae022c7 100644 --- a/mediapipe/calculators/core/split_vector_calculator.h +++ b/mediapipe/calculators/core/split_vector_calculator.h @@ -34,7 +34,9 @@ namespace mediapipe { // SplitVectorCalculatorOptions. If the option "element_only" is set to true, // all ranges should be of size 1 and all outputs will be elements of type T. If // "element_only" is false, ranges can be non-zero in size and all outputs will -// be of type std::vector. +// be of type std::vector. If the option "combine_outputs" is set to true, +// only one output stream can be specified and all ranges of elements will be +// combined into one vector. // To use this class for a particular type T, register a calculator using // SplitVectorCalculator. template @@ -49,28 +51,47 @@ class SplitVectorCalculator : public CalculatorBase { const auto& options = cc->Options<::mediapipe::SplitVectorCalculatorOptions>(); - if (cc->Outputs().NumEntries() != options.ranges_size()) { - return ::mediapipe::InvalidArgumentError( - "The number of output streams should match the number of ranges " - "specified in the CalculatorOptions."); - } - - // Set the output types for each output stream. - for (int i = 0; i < cc->Outputs().NumEntries(); ++i) { - if (options.ranges(i).begin() < 0 || options.ranges(i).end() < 0 || - options.ranges(i).begin() >= options.ranges(i).end()) { - return ::mediapipe::InvalidArgumentError( - "Indices should be non-negative and begin index should be less " - "than the end index."); - } - if (options.element_only()) { - if (options.ranges(i).end() - options.ranges(i).begin() != 1) { - return ::mediapipe::InvalidArgumentError( - "Since element_only is true, all ranges should be of size 1."); + if (options.combine_outputs()) { + RET_CHECK_EQ(cc->Outputs().NumEntries(), 1); + cc->Outputs().Index(0).Set>(); + for (int i = 0; i < options.ranges_size() - 1; ++i) { + for (int j = i + 1; j < options.ranges_size(); ++j) { + const auto& range_0 = options.ranges(i); + const auto& range_1 = options.ranges(j); + if ((range_0.begin() >= range_1.begin() && + range_0.begin() < range_1.end()) || + (range_1.begin() >= range_0.begin() && + range_1.begin() < range_0.end())) { + return ::mediapipe::InvalidArgumentError( + "Ranges must be non-overlapping when using combine_outputs " + "option."); + } + } + } + } else { + if (cc->Outputs().NumEntries() != options.ranges_size()) { + return ::mediapipe::InvalidArgumentError( + "The number of output streams should match the number of ranges " + "specified in the CalculatorOptions."); + } + + // Set the output types for each output stream. + for (int i = 0; i < cc->Outputs().NumEntries(); ++i) { + if (options.ranges(i).begin() < 0 || options.ranges(i).end() < 0 || + options.ranges(i).begin() >= options.ranges(i).end()) { + return ::mediapipe::InvalidArgumentError( + "Indices should be non-negative and begin index should be less " + "than the end index."); + } + if (options.element_only()) { + if (options.ranges(i).end() - options.ranges(i).begin() != 1) { + return ::mediapipe::InvalidArgumentError( + "Since element_only is true, all ranges should be of size 1."); + } + cc->Outputs().Index(i).Set(); + } else { + cc->Outputs().Index(i).Set>(); } - cc->Outputs().Index(i).Set(); - } else { - cc->Outputs().Index(i).Set>(); } } @@ -83,13 +104,15 @@ class SplitVectorCalculator : public CalculatorBase { const auto& options = cc->Options<::mediapipe::SplitVectorCalculatorOptions>(); + element_only_ = options.element_only(); + combine_outputs_ = options.combine_outputs(); + for (const auto& range : options.ranges()) { ranges_.push_back({range.begin(), range.end()}); max_range_end_ = std::max(max_range_end_, range.end()); + total_elements_ += range.end() - range.begin(); } - element_only_ = options.element_only(); - return ::mediapipe::OkStatus(); } @@ -97,17 +120,29 @@ class SplitVectorCalculator : public CalculatorBase { const auto& input = cc->Inputs().Index(0).Get>(); RET_CHECK_GE(input.size(), max_range_end_); - if (element_only_) { + if (combine_outputs_) { + auto output = absl::make_unique>(); + output->reserve(total_elements_); for (int i = 0; i < ranges_.size(); ++i) { - cc->Outputs().Index(i).AddPacket( - MakePacket(input[ranges_[i].first]).At(cc->InputTimestamp())); - } - } else { - for (int i = 0; i < ranges_.size(); ++i) { - auto output = absl::make_unique>( + auto elements = absl::make_unique>( input.begin() + ranges_[i].first, input.begin() + ranges_[i].second); - cc->Outputs().Index(i).Add(output.release(), cc->InputTimestamp()); + output->insert(output->end(), elements->begin(), elements->end()); + } + cc->Outputs().Index(0).Add(output.release(), cc->InputTimestamp()); + } else { + if (element_only_) { + for (int i = 0; i < ranges_.size(); ++i) { + cc->Outputs().Index(i).AddPacket( + MakePacket(input[ranges_[i].first]).At(cc->InputTimestamp())); + } + } else { + for (int i = 0; i < ranges_.size(); ++i) { + auto output = absl::make_unique>( + input.begin() + ranges_[i].first, + input.begin() + ranges_[i].second); + cc->Outputs().Index(i).Add(output.release(), cc->InputTimestamp()); + } } } @@ -117,7 +152,9 @@ class SplitVectorCalculator : public CalculatorBase { private: std::vector> ranges_; int32 max_range_end_ = -1; + int32 total_elements_ = 0; bool element_only_ = false; + bool combine_outputs_ = false; }; } // namespace mediapipe diff --git a/mediapipe/calculators/core/split_vector_calculator.proto b/mediapipe/calculators/core/split_vector_calculator.proto index 3ef31475b..53acbb7bf 100644 --- a/mediapipe/calculators/core/split_vector_calculator.proto +++ b/mediapipe/calculators/core/split_vector_calculator.proto @@ -37,4 +37,7 @@ message SplitVectorCalculatorOptions { // just element of type T. By default, if a range specifies only one element, // it is outputted as an std::vector. optional bool element_only = 2 [default = false]; + + // Combines output elements to one vector. + optional bool combine_outputs = 3 [default = false]; } diff --git a/mediapipe/calculators/core/split_vector_calculator_test.cc b/mediapipe/calculators/core/split_vector_calculator_test.cc index 4187e8aba..79243c149 100644 --- a/mediapipe/calculators/core/split_vector_calculator_test.cc +++ b/mediapipe/calculators/core/split_vector_calculator_test.cc @@ -105,6 +105,34 @@ class SplitTfLiteTensorVectorCalculatorTest : public ::testing::Test { } } + void ValidateCombinedVectorOutput(std::vector& output_packets, + int expected_elements, + std::vector& input_begin_indices, + std::vector& input_end_indices) { + ASSERT_EQ(1, output_packets.size()); + ASSERT_EQ(input_begin_indices.size(), input_end_indices.size()); + const std::vector& output_vec = + output_packets[0].Get>(); + ASSERT_EQ(expected_elements, output_vec.size()); + const int num_ranges = input_begin_indices.size(); + + int element_id = 0; + for (int range_id = 0; range_id < num_ranges; ++range_id) { + for (int i = input_begin_indices[range_id]; + i < input_end_indices[range_id]; ++i) { + const int expected_value = i; + const TfLiteTensor* result = &output_vec[element_id]; + float* result_buffer = result->data.f; + ASSERT_NE(result_buffer, nullptr); + ASSERT_EQ(result_buffer, input_buffers_[i]); + for (int j = 0; j < width * height * channels; ++j) { + ASSERT_EQ(expected_value, result_buffer[j]); + } + element_id++; + } + } + } + void ValidateElementOutput(std::vector& output_packets, int input_begin_index) { ASSERT_EQ(1, output_packets.size()); @@ -234,6 +262,65 @@ TEST_F(SplitTfLiteTensorVectorCalculatorTest, InvalidOutputStreamCountTest) { ASSERT_FALSE(graph.Initialize(graph_config).ok()); } +TEST_F(SplitTfLiteTensorVectorCalculatorTest, + InvalidCombineOutputsMultipleOutputsTest) { + ASSERT_NE(interpreter_, nullptr); + + // Prepare a graph to use the SplitTfLiteTensorVectorCalculator. + CalculatorGraphConfig graph_config = + ::mediapipe::ParseTextProtoOrDie( + R"( + input_stream: "tensor_in" + node { + calculator: "SplitTfLiteTensorVectorCalculator" + input_stream: "tensor_in" + output_stream: "range_0" + output_stream: "range_1" + options { + [mediapipe.SplitVectorCalculatorOptions.ext] { + ranges: { begin: 0 end: 1 } + ranges: { begin: 2 end: 3 } + combine_outputs: true + } + } + } + )"); + + // Run the graph. + CalculatorGraph graph; + // The graph should fail running because the number of output streams does not + // match the number of range elements in the options. + ASSERT_FALSE(graph.Initialize(graph_config).ok()); +} + +TEST_F(SplitTfLiteTensorVectorCalculatorTest, InvalidOverlappingRangesTest) { + ASSERT_NE(interpreter_, nullptr); + + // Prepare a graph to use the SplitTfLiteTensorVectorCalculator. + CalculatorGraphConfig graph_config = + ::mediapipe::ParseTextProtoOrDie( + R"( + input_stream: "tensor_in" + node { + calculator: "SplitTfLiteTensorVectorCalculator" + input_stream: "tensor_in" + output_stream: "range_0" + options { + [mediapipe.SplitVectorCalculatorOptions.ext] { + ranges: { begin: 0 end: 3 } + ranges: { begin: 1 end: 4 } + combine_outputs: true + } + } + } + )"); + + // Run the graph. + CalculatorGraph graph; + // The graph should fail running because there are overlapping ranges. + ASSERT_FALSE(graph.Initialize(graph_config).ok()); +} + TEST_F(SplitTfLiteTensorVectorCalculatorTest, SmokeTestElementOnly) { ASSERT_NE(interpreter_, nullptr); @@ -289,6 +376,53 @@ TEST_F(SplitTfLiteTensorVectorCalculatorTest, SmokeTestElementOnly) { MP_ASSERT_OK(graph.WaitUntilDone()); } +TEST_F(SplitTfLiteTensorVectorCalculatorTest, SmokeTestCombiningOutputs) { + ASSERT_NE(interpreter_, nullptr); + + PrepareTfLiteTensorVector(/*vector_size=*/5); + ASSERT_NE(input_vec_, nullptr); + + // Prepare a graph to use the SplitTfLiteTensorVectorCalculator. + CalculatorGraphConfig graph_config = + ::mediapipe::ParseTextProtoOrDie( + R"( + input_stream: "tensor_in" + node { + calculator: "SplitTfLiteTensorVectorCalculator" + input_stream: "tensor_in" + output_stream: "range_0" + options { + [mediapipe.SplitVectorCalculatorOptions.ext] { + ranges: { begin: 0 end: 1 } + ranges: { begin: 2 end: 3 } + ranges: { begin: 4 end: 5 } + combine_outputs: true + } + } + } + )"); + std::vector range_0_packets; + tool::AddVectorSink("range_0", &graph_config, &range_0_packets); + + // Run the graph. + CalculatorGraph graph; + MP_ASSERT_OK(graph.Initialize(graph_config)); + MP_ASSERT_OK(graph.StartRun({})); + MP_ASSERT_OK(graph.AddPacketToInputStream( + "tensor_in", Adopt(input_vec_.release()).At(Timestamp(0)))); + // Wait until the calculator finishes processing. + MP_ASSERT_OK(graph.WaitUntilIdle()); + + std::vector input_begin_indices = {0, 2, 4}; + std::vector input_end_indices = {1, 3, 5}; + ValidateCombinedVectorOutput(range_0_packets, /*expected_elements=*/3, + input_begin_indices, input_end_indices); + + // Fully close the graph at the end. + MP_ASSERT_OK(graph.CloseInputStream("tensor_in")); + MP_ASSERT_OK(graph.WaitUntilDone()); +} + TEST_F(SplitTfLiteTensorVectorCalculatorTest, ElementOnlyDisablesVectorOutputs) { // Prepare a graph to use the SplitTfLiteTensorVectorCalculator. diff --git a/mediapipe/calculators/core/string_to_int_calculator.cc b/mediapipe/calculators/core/string_to_int_calculator.cc new file mode 100644 index 000000000..64600cde3 --- /dev/null +++ b/mediapipe/calculators/core/string_to_int_calculator.cc @@ -0,0 +1,79 @@ +// Copyright 2019 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/strings/numbers.h" +#include "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/port/integral_types.h" +#include "mediapipe/framework/port/status.h" + +namespace mediapipe { + +// Calculator that converts a std::string into an integer type, or fails if the +// conversion is not possible. +// +// Example config: +// node { +// calculator: "StringToIntCalculator" +// input_side_packet: "string" +// output_side_packet: "index" +// } +template +class StringToIntCalculatorTemplate : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->InputSidePackets().Index(0).Set(); + cc->OutputSidePackets().Index(0).Set(); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Open(CalculatorContext* cc) override { + IntType number; + if (!absl::SimpleAtoi(cc->InputSidePackets().Index(0).Get(), + &number)) { + return ::mediapipe::InvalidArgumentError( + "The std::string could not be parsed as an integer."); + } + cc->OutputSidePackets().Index(0).Set(MakePacket(number)); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) override { + return ::mediapipe::OkStatus(); + } +}; + +using StringToIntCalculator = StringToIntCalculatorTemplate; +REGISTER_CALCULATOR(StringToIntCalculator); + +using StringToUintCalculator = StringToIntCalculatorTemplate; +REGISTER_CALCULATOR(StringToUintCalculator); + +using StringToInt32Calculator = StringToIntCalculatorTemplate; +REGISTER_CALCULATOR(StringToInt32Calculator); + +using StringToUint32Calculator = StringToIntCalculatorTemplate; +REGISTER_CALCULATOR(StringToUint32Calculator); + +using StringToInt64Calculator = StringToIntCalculatorTemplate; +REGISTER_CALCULATOR(StringToInt64Calculator); + +using StringToUint64Calculator = StringToIntCalculatorTemplate; +REGISTER_CALCULATOR(StringToUint64Calculator); + +} // namespace mediapipe diff --git a/mediapipe/calculators/tensorflow/BUILD b/mediapipe/calculators/tensorflow/BUILD index 4231b899e..c9c505495 100644 --- a/mediapipe/calculators/tensorflow/BUILD +++ b/mediapipe/calculators/tensorflow/BUILD @@ -104,6 +104,17 @@ proto_library( deps = ["//mediapipe/framework:calculator_proto"], ) +proto_library( + name = "unpack_media_sequence_calculator_proto", + srcs = ["unpack_media_sequence_calculator.proto"], + visibility = ["//visibility:public"], + deps = [ + "//mediapipe/calculators/core:packet_resampler_calculator_proto", + "//mediapipe/framework:calculator_proto", + "//mediapipe/util:audio_decoder_proto", + ], +) + proto_library( name = "vector_float_to_tensor_calculator_options_proto", srcs = ["vector_float_to_tensor_calculator_options.proto"], @@ -261,6 +272,17 @@ mediapipe_cc_proto_library( deps = [":unpack_media_sequence_calculator_proto"], ) +mediapipe_cc_proto_library( + name = "vector_int_to_tensor_calculator_options_cc_proto", + srcs = ["vector_int_to_tensor_calculator_options.proto"], + cc_deps = [ + "//mediapipe/framework:calculator_cc_proto", + "@org_tensorflow//tensorflow/core:protos_all_cc", + ], + visibility = ["//visibility:public"], + deps = [":vector_int_to_tensor_calculator_options_proto"], +) + mediapipe_cc_proto_library( name = "vector_float_to_tensor_calculator_options_cc_proto", srcs = ["vector_float_to_tensor_calculator_options.proto"], @@ -621,6 +643,22 @@ cc_library( alwayslink = 1, ) +cc_library( + name = "tfrecord_reader_calculator", + srcs = ["tfrecord_reader_calculator.cc"], + visibility = ["//visibility:public"], + deps = [ + "//mediapipe/framework:calculator_framework", + "//mediapipe/framework/port:integral_types", + "//mediapipe/framework/port:logging", + "//mediapipe/framework/port:ret_check", + "//mediapipe/framework/port:status", + "@org_tensorflow//tensorflow/core:lib", + "@org_tensorflow//tensorflow/core:protos_all_cc", + ], + alwayslink = 1, +) + cc_library( name = "tensor_to_vector_float_calculator", srcs = ["tensor_to_vector_float_calculator.cc"], @@ -662,6 +700,20 @@ cc_library( alwayslink = 1, ) +cc_library( + name = "vector_int_to_tensor_calculator", + srcs = ["vector_int_to_tensor_calculator.cc"], + visibility = ["//visibility:public"], + deps = [ + ":vector_int_to_tensor_calculator_options_cc_proto", + "//mediapipe/framework:calculator_framework", + "//mediapipe/framework/port:ret_check", + "//mediapipe/framework/port:status", + "@org_tensorflow//tensorflow/core:framework", + ], + alwayslink = 1, +) + cc_library( name = "vector_float_to_tensor_calculator", srcs = ["vector_float_to_tensor_calculator.cc"], @@ -676,6 +728,20 @@ cc_library( alwayslink = 1, ) +cc_library( + name = "unpack_yt8m_sequence_example_calculator", + srcs = ["unpack_yt8m_sequence_example_calculator.cc"], + visibility = ["//visibility:public"], + deps = [ + "//mediapipe/calculators/tensorflow:lapped_tensor_buffer_calculator_cc_proto", + "//mediapipe/framework:calculator_framework", + "//mediapipe/framework:packet", + "//mediapipe/framework/port:status", + "@org_tensorflow//tensorflow/core:protos_all_cc", + ], + alwayslink = 1, +) + cc_test( name = "graph_tensors_packet_generator_test", srcs = ["graph_tensors_packet_generator_test.cc"], @@ -980,6 +1046,20 @@ cc_test( ], ) +cc_test( + name = "vector_int_to_tensor_calculator_test", + srcs = ["vector_int_to_tensor_calculator_test.cc"], + deps = [ + ":vector_int_to_tensor_calculator", + ":vector_int_to_tensor_calculator_options_cc_proto", + "//mediapipe/framework:calculator_framework", + "//mediapipe/framework:calculator_runner", + "//mediapipe/framework/port:gtest_main", + "@org_tensorflow//tensorflow/core:framework", + "@org_tensorflow//tensorflow/core:protos_all_cc", + ], +) + cc_test( name = "vector_float_to_tensor_calculator_test", srcs = ["vector_float_to_tensor_calculator_test.cc"], diff --git a/mediapipe/calculators/tensorflow/lapped_tensor_buffer_calculator.cc b/mediapipe/calculators/tensorflow/lapped_tensor_buffer_calculator.cc index 78ee50871..5ad8e853c 100644 --- a/mediapipe/calculators/tensorflow/lapped_tensor_buffer_calculator.cc +++ b/mediapipe/calculators/tensorflow/lapped_tensor_buffer_calculator.cc @@ -29,6 +29,11 @@ namespace mediapipe { +const char kBufferSize[] = "BUFFER_SIZE"; +const char kOverlap[] = "OVERLAP"; +const char kTimestampOffset[] = "TIMESTAMP_OFFSET"; +const char kCalculatorOptions[] = "CALCULATOR_OPTIONS"; + namespace tf = tensorflow; // Given an input stream of tensors, concatenates the tensors over timesteps. @@ -72,6 +77,9 @@ class LappedTensorBufferCalculator : public CalculatorBase { ::mediapipe::Status AddBatchDimension(tf::Tensor* input_tensor); int steps_until_output_; + int buffer_size_; + int overlap_; + int timestamp_offset_; std::unique_ptr> timestamp_buffer_; std::unique_ptr> buffer_; LappedTensorBufferCalculatorOptions options_; @@ -87,6 +95,21 @@ REGISTER_CALCULATOR(LappedTensorBufferCalculator); ); RET_CHECK_EQ(cc->Inputs().NumEntries(), 1) << "Only one output stream is supported."; + + if (cc->InputSidePackets().HasTag(kBufferSize)) { + cc->InputSidePackets().Tag(kBufferSize).Set(); + } + if (cc->InputSidePackets().HasTag(kOverlap)) { + cc->InputSidePackets().Tag(kOverlap).Set(); + } + if (cc->InputSidePackets().HasTag(kTimestampOffset)) { + cc->InputSidePackets().Tag(kTimestampOffset).Set(); + } + if (cc->InputSidePackets().HasTag(kCalculatorOptions)) { + cc->InputSidePackets() + .Tag(kCalculatorOptions) + .Set(); + } cc->Outputs().Index(0).Set( // Output tensorflow::Tensor stream with possibly overlapping steps. ); @@ -95,16 +118,33 @@ REGISTER_CALCULATOR(LappedTensorBufferCalculator); ::mediapipe::Status LappedTensorBufferCalculator::Open(CalculatorContext* cc) { options_ = cc->Options(); - RET_CHECK_LT(options_.overlap(), options_.buffer_size()); - RET_CHECK_GE(options_.timestamp_offset(), 0) + if (cc->InputSidePackets().HasTag(kCalculatorOptions)) { + options_ = cc->InputSidePackets() + .Tag(kCalculatorOptions) + .Get(); + } + buffer_size_ = options_.buffer_size(); + if (cc->InputSidePackets().HasTag(kBufferSize)) { + buffer_size_ = cc->InputSidePackets().Tag(kBufferSize).Get(); + } + overlap_ = options_.overlap(); + if (cc->InputSidePackets().HasTag(kOverlap)) { + overlap_ = cc->InputSidePackets().Tag(kOverlap).Get(); + } + timestamp_offset_ = options_.timestamp_offset(); + if (cc->InputSidePackets().HasTag(kTimestampOffset)) { + timestamp_offset_ = cc->InputSidePackets().Tag(kTimestampOffset).Get(); + } + + RET_CHECK_LT(overlap_, buffer_size_); + RET_CHECK_GE(timestamp_offset_, 0) << "Negative timestamp_offset is not allowed."; - RET_CHECK_LT(options_.timestamp_offset(), options_.buffer_size()) + RET_CHECK_LT(timestamp_offset_, buffer_size_) << "output_frame_num_offset has to be less than buffer_size."; timestamp_buffer_ = - absl::make_unique>(options_.buffer_size()); - buffer_ = - absl::make_unique>(options_.buffer_size()); - steps_until_output_ = options_.buffer_size(); + absl::make_unique>(buffer_size_); + buffer_ = absl::make_unique>(buffer_size_); + steps_until_output_ = buffer_size_; return ::mediapipe::OkStatus(); } @@ -128,11 +168,10 @@ REGISTER_CALCULATOR(LappedTensorBufferCalculator); concatenated.get()); RET_CHECK(concat_status.ok()) << concat_status.ToString(); - cc->Outputs().Index(0).Add( - concatenated.release(), - timestamp_buffer_->Get(options_.timestamp_offset())); + cc->Outputs().Index(0).Add(concatenated.release(), + timestamp_buffer_->Get(timestamp_offset_)); - steps_until_output_ = options_.buffer_size() - options_.overlap(); + steps_until_output_ = buffer_size_ - overlap_; } return ::mediapipe::OkStatus(); } diff --git a/mediapipe/calculators/tensorflow/tfrecord_reader_calculator.cc b/mediapipe/calculators/tensorflow/tfrecord_reader_calculator.cc new file mode 100644 index 000000000..5de7b0c0d --- /dev/null +++ b/mediapipe/calculators/tensorflow/tfrecord_reader_calculator.cc @@ -0,0 +1,126 @@ +// Copyright 2019 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 "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/port/integral_types.h" +#include "mediapipe/framework/port/logging.h" +#include "mediapipe/framework/port/ret_check.h" +#include "mediapipe/framework/port/status.h" +#include "tensorflow/core/example/example.pb.h" +#include "tensorflow/core/lib/core/status.h" +#include "tensorflow/core/lib/io/record_reader.h" +#include "tensorflow/core/platform/env.h" +#include "tensorflow/core/platform/file_system.h" + +namespace mediapipe { + +const char kTFRecordPath[] = "TFRECORD_PATH"; +const char kRecordIndex[] = "RECORD_INDEX"; +const char kExampleTag[] = "EXAMPLE"; +const char kSequenceExampleTag[] = "SEQUENCE_EXAMPLE"; + +// Reads a tensorflow example/sequence example from a tfrecord file. +// If the "RECORD_INDEX" input side packet is provided, the calculator is going +// to fetch the example/sequence example of the tfrecord file at the target +// record index. Otherwise, the reader always reads the first example/sequence +// example of the tfrecord file. +// +// Example config: +// node { +// calculator: "TFRecordReaderCalculator" +// input_side_packet: "TFRECORD_PATH:tfrecord_path" +// input_side_packet: "RECORD_INDEX:record_index" +// output_side_packet: "SEQUENCE_EXAMPLE:sequence_example" +// } +class TFRecordReaderCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc); + + ::mediapipe::Status Open(CalculatorContext* cc) override; + ::mediapipe::Status Process(CalculatorContext* cc) override; +}; + +::mediapipe::Status TFRecordReaderCalculator::GetContract( + CalculatorContract* cc) { + cc->InputSidePackets().Tag(kTFRecordPath).Set(); + if (cc->InputSidePackets().HasTag(kRecordIndex)) { + cc->InputSidePackets().Tag(kRecordIndex).Set(); + } + + RET_CHECK(cc->OutputSidePackets().HasTag(kExampleTag) || + cc->OutputSidePackets().HasTag(kSequenceExampleTag)) + << "TFRecordReaderCalculator must output either Tensorflow example or " + "sequence example."; + if (cc->OutputSidePackets().HasTag(kExampleTag)) { + cc->OutputSidePackets().Tag(kExampleTag).Set(); + } else { + cc->OutputSidePackets() + .Tag(kSequenceExampleTag) + .Set(); + } + return ::mediapipe::OkStatus(); +} + +::mediapipe::Status TFRecordReaderCalculator::Open(CalculatorContext* cc) { + std::unique_ptr file; + auto tf_status = tensorflow::Env::Default()->NewRandomAccessFile( + cc->InputSidePackets().Tag(kTFRecordPath).Get(), &file); + RET_CHECK(tf_status.ok()) + << "Failed to open tfrecord file: " << tf_status.error_message(); + tensorflow::io::RecordReader reader(file.get(), + tensorflow::io::RecordReaderOptions()); + tensorflow::uint64 offset = 0; + std::string example_str; + const int target_idx = + cc->InputSidePackets().HasTag(kRecordIndex) + ? cc->InputSidePackets().Tag(kRecordIndex).Get() + : 0; + int current_idx = 0; + while (current_idx <= target_idx) { + tf_status = reader.ReadRecord(&offset, &example_str); + RET_CHECK(tf_status.ok()) + << "Failed to read tfrecord: " << tf_status.error_message(); + if (current_idx == target_idx) { + if (cc->OutputSidePackets().HasTag(kExampleTag)) { + tensorflow::Example tf_example; + tf_example.ParseFromString(example_str); + cc->OutputSidePackets() + .Tag(kExampleTag) + .Set(MakePacket(std::move(tf_example))); + } else { + tensorflow::SequenceExample tf_sequence_example; + tf_sequence_example.ParseFromString(example_str); + cc->OutputSidePackets() + .Tag(kSequenceExampleTag) + .Set(MakePacket( + std::move(tf_sequence_example))); + } + } + ++current_idx; + } + + return ::mediapipe::OkStatus(); +} + +::mediapipe::Status TFRecordReaderCalculator::Process(CalculatorContext* cc) { + return ::mediapipe::OkStatus(); +} + +REGISTER_CALCULATOR(TFRecordReaderCalculator); + +} // namespace mediapipe diff --git a/mediapipe/calculators/tensorflow/unpack_yt8m_sequence_example_calculator.cc b/mediapipe/calculators/tensorflow/unpack_yt8m_sequence_example_calculator.cc new file mode 100644 index 000000000..daf7f1117 --- /dev/null +++ b/mediapipe/calculators/tensorflow/unpack_yt8m_sequence_example_calculator.cc @@ -0,0 +1,192 @@ +// Copyright 2019 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 "mediapipe/calculators/tensorflow/lapped_tensor_buffer_calculator.pb.h" +#include "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/packet.h" +#include "mediapipe/framework/port/canonical_errors.h" +#include "tensorflow/core/example/example.pb.h" +#include "tensorflow/core/example/feature.pb.h" + +namespace mediapipe { +namespace { + +const char kId[] = "id"; +const char kRgb[] = "rgb"; +const char kAudio[] = "audio"; +const char kDesiredSegmentSize[] = "DESIRED_SEGMENT_SIZE"; +const char kYt8mId[] = "YT8M_ID"; +const char kYt8mSequenceExample[] = "YT8M_SEQUENCE_EXAMPLE"; +const char kQuantizedRgbFeature[] = "QUANTIZED_RGB_FEATURE"; +const char kQuantizedAudioFeature[] = "QUANTIZED_AUDIO_FEATURE"; +const char kSegmentSize[] = "SEGMENT_SIZE"; +const char kLappedTensorBufferCalculatorOptions[] = + "LAPPED_TENSOR_BUFFER_CALCULATOR_OPTIONS"; + +std::string GetQuantizedFeature( + const tensorflow::SequenceExample& sequence_example, const std::string& key, + int index) { + const auto& bytes_list = sequence_example.feature_lists() + .feature_list() + .at(key) + .feature() + .Get(index) + .bytes_list() + .value(); + CHECK_EQ(1, bytes_list.size()); + return bytes_list.Get(0); +} +} // namespace + +// Unpacks YT8M Sequence Example. Note that the audio feature and rgb feature +// output are quantized. DequantizeByteArrayCalculator can do the dequantization +// for you. +// +// Example config: +// node { +// calculator: "UnpackYt8mSequenceExampleCalculator" +// input_side_packet: "YT8M_SEQUENCE_EXAMPLE:yt8m_sequence_example" +// output_stream: "QUANTIZED_RGB_FEATURE:quantized_rgb_feature" +// output_stream: "QUANTIZED_AUDIO_FEATURE:quantized_audio_feature" +// } +class UnpackYt8mSequenceExampleCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->InputSidePackets() + .Tag(kYt8mSequenceExample) + .Set(); + if (cc->InputSidePackets().HasTag(kDesiredSegmentSize)) { + cc->InputSidePackets().Tag(kDesiredSegmentSize).Set(); + } + cc->Outputs().Tag(kQuantizedRgbFeature).Set(); + cc->Outputs().Tag(kQuantizedAudioFeature).Set(); + if (cc->OutputSidePackets().HasTag(kYt8mId)) { + cc->OutputSidePackets().Tag(kYt8mId).Set(); + } + if (cc->OutputSidePackets().HasTag(kLappedTensorBufferCalculatorOptions)) { + cc->OutputSidePackets() + .Tag(kLappedTensorBufferCalculatorOptions) + .Set<::mediapipe::LappedTensorBufferCalculatorOptions>(); + } + if (cc->OutputSidePackets().HasTag(kSegmentSize)) { + cc->OutputSidePackets().Tag(kSegmentSize).Set(); + } + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Open(CalculatorContext* cc) override { + const tensorflow::SequenceExample& sequence_example = + cc->InputSidePackets() + .Tag(kYt8mSequenceExample) + .Get(); + const std::string& yt8m_id = + sequence_example.context().feature().at(kId).bytes_list().value().Get( + 0); + if (cc->OutputSidePackets().HasTag(kYt8mId)) { + cc->OutputSidePackets().Tag(kYt8mId).Set( + MakePacket(yt8m_id)); + } + + int rgb_feature_list_length = + sequence_example.feature_lists().feature_list().at(kRgb).feature_size(); + int audio_feature_list_length = sequence_example.feature_lists() + .feature_list() + .at(kAudio) + .feature_size(); + + if (rgb_feature_list_length != audio_feature_list_length) { + return ::mediapipe::FailedPreconditionError(absl::StrCat( + "Data corruption: the length of audio features and rgb features are " + "not equal. Please check the sequence example that contains yt8m " + "id: ", + yt8m_id)); + } + feature_list_length_ = rgb_feature_list_length; + if (cc->OutputSidePackets().HasTag(kLappedTensorBufferCalculatorOptions) || + cc->OutputSidePackets().HasTag(kSegmentSize)) { + // If the desired segment size is specified, take the min of the length of + // the feature list and the desired size to be the output segment size. + int segment_size = feature_list_length_; + if (cc->InputSidePackets().HasTag(kDesiredSegmentSize)) { + int desired_segment_size = + cc->InputSidePackets().Tag(kDesiredSegmentSize).Get(); + RET_CHECK(desired_segment_size > 0) + << "The desired segment size must be greater than zero."; + segment_size = std::min( + feature_list_length_, + cc->InputSidePackets().Tag(kDesiredSegmentSize).Get()); + } + if (cc->OutputSidePackets().HasTag( + kLappedTensorBufferCalculatorOptions)) { + auto lapped_tensor_buffer_calculator_options = absl::make_unique< + ::mediapipe::LappedTensorBufferCalculatorOptions>(); + lapped_tensor_buffer_calculator_options->set_add_batch_dim_to_tensors( + true); + lapped_tensor_buffer_calculator_options->set_buffer_size(segment_size); + lapped_tensor_buffer_calculator_options->set_overlap(segment_size - 1); + lapped_tensor_buffer_calculator_options->set_timestamp_offset( + segment_size - 1); + cc->OutputSidePackets() + .Tag(kLappedTensorBufferCalculatorOptions) + .Set(Adopt(lapped_tensor_buffer_calculator_options.release())); + } + if (cc->OutputSidePackets().HasTag(kSegmentSize)) { + cc->OutputSidePackets() + .Tag(kSegmentSize) + .Set(MakePacket(segment_size)); + } + } + LOG(INFO) << "Reading the sequence example that contains yt8m id: " + << yt8m_id << ". Feature list length: " << feature_list_length_; + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) override { + if (current_index_ >= feature_list_length_) { + return ::mediapipe::tool::StatusStop(); + } + const tensorflow::SequenceExample& sequence_example = + cc->InputSidePackets() + .Tag(kYt8mSequenceExample) + .Get(); + + // Uses microsecond as the unit of time. In the YT8M dataset, each feature + // represents a second. + const Timestamp timestamp = Timestamp(current_index_ * 1000000); + cc->Outputs() + .Tag(kQuantizedRgbFeature) + .AddPacket( + MakePacket( + GetQuantizedFeature(sequence_example, kRgb, current_index_)) + .At(timestamp)); + cc->Outputs() + .Tag(kQuantizedAudioFeature) + .AddPacket( + MakePacket( + GetQuantizedFeature(sequence_example, kAudio, current_index_)) + .At(timestamp)); + ++current_index_; + return ::mediapipe::OkStatus(); + } + + private: + int current_index_ = 0; + int feature_list_length_ = 0; +}; + +REGISTER_CALCULATOR(UnpackYt8mSequenceExampleCalculator); + +} // namespace mediapipe diff --git a/mediapipe/calculators/tensorflow/vector_float_to_tensor_calculator.cc b/mediapipe/calculators/tensorflow/vector_float_to_tensor_calculator.cc index a96e39918..068be5714 100644 --- a/mediapipe/calculators/tensorflow/vector_float_to_tensor_calculator.cc +++ b/mediapipe/calculators/tensorflow/vector_float_to_tensor_calculator.cc @@ -23,10 +23,12 @@ namespace mediapipe { -namespace tf = ::tensorflow; - +namespace { auto& INPUT_1D = VectorFloatToTensorCalculatorOptions::INPUT_1D; auto& INPUT_2D = VectorFloatToTensorCalculatorOptions::INPUT_2D; +} // namespace + +namespace tf = ::tensorflow; // The calculator expects one input (a packet containing a vector or // vector>) and generates one output (a packet containing a diff --git a/mediapipe/calculators/tensorflow/vector_int_to_tensor_calculator.cc b/mediapipe/calculators/tensorflow/vector_int_to_tensor_calculator.cc new file mode 100644 index 000000000..1269e2761 --- /dev/null +++ b/mediapipe/calculators/tensorflow/vector_int_to_tensor_calculator.cc @@ -0,0 +1,203 @@ +// Copyright 2019 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. +// +// Converts a single int or vector or vector> to 1D (or 2D) +// tf::Tensor. + +#include "mediapipe/calculators/tensorflow/vector_int_to_tensor_calculator_options.pb.h" +#include "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/port/ret_check.h" +#include "mediapipe/framework/port/status.h" +#include "tensorflow/core/framework/tensor.h" +#include "tensorflow/core/framework/types.h" + +namespace mediapipe { + +const char kVectorInt[] = "VECTOR_INT"; +const char kSingleInt[] = "SINGLE_INT"; +const char kTensorOut[] = "TENSOR_OUT"; + +namespace { +auto& INPUT_1D = VectorIntToTensorCalculatorOptions::INPUT_1D; +auto& INPUT_2D = VectorIntToTensorCalculatorOptions::INPUT_2D; +} // namespace + +namespace tf = ::tensorflow; + +template +void AssignMatrixValue(int r, int c, int value, tf::Tensor* output_tensor) { + output_tensor->tensor()(r, c) = value; +} + +// The calculator expects one input (a packet containing a single int or +// vector or vector>) and generates one output (a packet +// containing a tf::Tensor containing the same data). The output tensor will be +// either 1D or 2D with dimensions corresponding to the input vector int. It +// will hold DT_INT32 or DT_UINT8 or DT_INT64 values. +// +// Example config: +// node { +// calculator: "VectorIntToTensorCalculator" +// input_stream: "SINGLE_INT:segment_size_int_stream" +// output_stream: "TENSOR_OUT:segment_size_tensor" +// } +// +// or +// +// node { +// calculator: "VectorIntToTensorCalculator" +// input_stream: "VECTOR_INT:vector_int_features" +// output_stream: "TENSOR_OUT:tensor_features" +// } +class VectorIntToTensorCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc); + + ::mediapipe::Status Open(CalculatorContext* cc) override; + ::mediapipe::Status Process(CalculatorContext* cc) override; + + private: + VectorIntToTensorCalculatorOptions options_; +}; +REGISTER_CALCULATOR(VectorIntToTensorCalculator); + +::mediapipe::Status VectorIntToTensorCalculator::GetContract( + CalculatorContract* cc) { + const auto& options = cc->Options(); + // Start with only one input packet. + RET_CHECK_EQ(cc->Inputs().NumEntries(), 1) + << "Only one input stream is supported."; + if (options.input_size() == INPUT_2D) { + cc->Inputs().Tag(kVectorInt).Set>>(); + } else if (options.input_size() == INPUT_1D) { + if (cc->Inputs().HasTag(kSingleInt)) { + cc->Inputs().Tag(kSingleInt).Set(); + } else { + cc->Inputs().Tag(kVectorInt).Set>(); + } + } else { + LOG(FATAL) << "input size not supported"; + } + RET_CHECK_EQ(cc->Outputs().NumEntries(), 1) + << "Only one output stream is supported."; + cc->Outputs().Tag(kTensorOut).Set(); + return ::mediapipe::OkStatus(); +} + +::mediapipe::Status VectorIntToTensorCalculator::Open(CalculatorContext* cc) { + options_ = cc->Options(); + RET_CHECK(options_.tensor_data_type() == tf::DT_UINT8 || + options_.tensor_data_type() == tf::DT_INT32 || + options_.tensor_data_type() == tf::DT_INT64) + << "Output tensor data type is not supported."; + return ::mediapipe::OkStatus(); +} + +::mediapipe::Status VectorIntToTensorCalculator::Process( + CalculatorContext* cc) { + tf::TensorShape tensor_shape; + if (options_.input_size() == INPUT_2D) { + const std::vector>& input = + cc->Inputs() + .Tag(kVectorInt) + .Value() + .Get>>(); + + const int32 rows = input.size(); + CHECK_GE(rows, 1); + const int32 cols = input[0].size(); + CHECK_GE(cols, 1); + for (int i = 1; i < rows; ++i) { + CHECK_EQ(input[i].size(), cols); + } + if (options_.transpose()) { + tensor_shape = tf::TensorShape({cols, rows}); + } else { + tensor_shape = tf::TensorShape({rows, cols}); + } + auto output = ::absl::make_unique(options_.tensor_data_type(), + tensor_shape); + if (options_.transpose()) { + for (int r = 0; r < rows; ++r) { + for (int c = 0; c < cols; ++c) { + switch (options_.tensor_data_type()) { + case tf::DT_INT64: + AssignMatrixValue(c, r, input[r][c], output.get()); + break; + case tf::DT_UINT8: + AssignMatrixValue(c, r, input[r][c], output.get()); + break; + case tf::DT_INT32: + AssignMatrixValue(c, r, input[r][c], output.get()); + break; + default: + LOG(FATAL) << "tensor data type is not supported."; + } + } + } + } else { + for (int r = 0; r < rows; ++r) { + for (int c = 0; c < cols; ++c) { + switch (options_.tensor_data_type()) { + case tf::DT_INT64: + AssignMatrixValue(r, c, input[r][c], output.get()); + break; + case tf::DT_UINT8: + AssignMatrixValue(r, c, input[r][c], output.get()); + break; + case tf::DT_INT32: + AssignMatrixValue(r, c, input[r][c], output.get()); + break; + default: + LOG(FATAL) << "tensor data type is not supported."; + } + } + } + } + cc->Outputs().Tag(kTensorOut).Add(output.release(), cc->InputTimestamp()); + } else if (options_.input_size() == INPUT_1D) { + std::vector input; + if (cc->Inputs().HasTag(kSingleInt)) { + input.push_back(cc->Inputs().Tag(kSingleInt).Get()); + } else { + input = cc->Inputs().Tag(kVectorInt).Value().Get>(); + } + CHECK_GE(input.size(), 1); + const int32 length = input.size(); + tensor_shape = tf::TensorShape({length}); + auto output = ::absl::make_unique(options_.tensor_data_type(), + tensor_shape); + for (int i = 0; i < length; ++i) { + switch (options_.tensor_data_type()) { + case tf::DT_INT64: + output->tensor()(i) = input.at(i); + break; + case tf::DT_UINT8: + output->tensor()(i) = input.at(i); + break; + case tf::DT_INT32: + output->tensor()(i) = input.at(i); + break; + default: + LOG(FATAL) << "tensor data type is not supported."; + } + } + cc->Outputs().Tag(kTensorOut).Add(output.release(), cc->InputTimestamp()); + } else { + LOG(FATAL) << "input size not supported"; + } + return ::mediapipe::OkStatus(); +} + +} // namespace mediapipe diff --git a/mediapipe/calculators/tensorflow/vector_int_to_tensor_calculator_options.proto b/mediapipe/calculators/tensorflow/vector_int_to_tensor_calculator_options.proto new file mode 100644 index 000000000..65554bb14 --- /dev/null +++ b/mediapipe/calculators/tensorflow/vector_int_to_tensor_calculator_options.proto @@ -0,0 +1,43 @@ +// Copyright 2019 The MediaPipe Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package mediapipe; + +import "mediapipe/framework/calculator.proto"; +import "tensorflow/core/framework/types.proto"; + +message VectorIntToTensorCalculatorOptions { + extend mediapipe.CalculatorOptions { + optional VectorIntToTensorCalculatorOptions ext = 275364184; + } + enum InputSize { + UNKNOWN = 0; + INPUT_1D = 1; + INPUT_2D = 2; + } + + // If input_size is INPUT_2D, unpack a vector> to a + // 2d tensor (matrix). If INPUT_1D, convert a single int or vector + // into a 1d tensor (vector). + optional InputSize input_size = 1 [default = INPUT_1D]; + + // If true, the output tensor is transposed. + // Otherwise, the output tensor is not transposed. + // It will be ignored if tensor_is_2d is INPUT_1D. + optional bool transpose = 2 [default = false]; + + optional tensorflow.DataType tensor_data_type = 3 [default = DT_INT32]; +} diff --git a/mediapipe/calculators/tensorflow/vector_int_to_tensor_calculator_test.cc b/mediapipe/calculators/tensorflow/vector_int_to_tensor_calculator_test.cc new file mode 100644 index 000000000..052a78516 --- /dev/null +++ b/mediapipe/calculators/tensorflow/vector_int_to_tensor_calculator_test.cc @@ -0,0 +1,202 @@ +// Copyright 2018 The MediaPipe Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "mediapipe/calculators/tensorflow/vector_int_to_tensor_calculator_options.pb.h" +#include "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/calculator_runner.h" +#include "mediapipe/framework/port/gmock.h" +#include "mediapipe/framework/port/gtest.h" +#include "tensorflow/core/framework/tensor.h" +#include "tensorflow/core/framework/types.pb.h" + +namespace mediapipe { + +namespace { + +namespace tf = ::tensorflow; + +class VectorIntToTensorCalculatorTest : public ::testing::Test { + protected: + void SetUpRunner( + const VectorIntToTensorCalculatorOptions::InputSize input_size, + const tensorflow::DataType tensor_data_type, const bool transpose, + const bool single_value) { + CalculatorGraphConfig::Node config; + config.set_calculator("VectorIntToTensorCalculator"); + if (single_value) { + config.add_input_stream("SINGLE_INT:input_int"); + } else { + config.add_input_stream("VECTOR_INT:input_int"); + } + config.add_output_stream("TENSOR_OUT:output_tensor"); + auto options = config.mutable_options()->MutableExtension( + VectorIntToTensorCalculatorOptions::ext); + options->set_input_size(input_size); + options->set_transpose(transpose); + options->set_tensor_data_type(tensor_data_type); + runner_ = ::absl::make_unique(config); + } + + void TestConvertFromVectoVectorInt(const bool transpose) { + SetUpRunner(VectorIntToTensorCalculatorOptions::INPUT_2D, + tensorflow::DT_INT32, transpose, false); + auto input = ::absl::make_unique>>( + 2, std::vector(2)); + for (int i = 0; i < 2; ++i) { + for (int j = 0; j < 2; ++j) { + input->at(i).at(j) = i * 2 + j; + } + } + + const int64 time = 1234; + runner_->MutableInputs() + ->Tag("VECTOR_INT") + .packets.push_back(Adopt(input.release()).At(Timestamp(time))); + + EXPECT_TRUE(runner_->Run().ok()); + + const std::vector& output_packets = + runner_->Outputs().Tag("TENSOR_OUT").packets; + EXPECT_EQ(1, output_packets.size()); + EXPECT_EQ(time, output_packets[0].Timestamp().Value()); + const tf::Tensor& output_tensor = output_packets[0].Get(); + + EXPECT_EQ(2, output_tensor.dims()); + EXPECT_EQ(tf::DT_INT32, output_tensor.dtype()); + const auto matrix = output_tensor.matrix(); + + for (int i = 0; i < 2; ++i) { + for (int j = 0; j < 2; ++j) { + if (!transpose) { + EXPECT_EQ(i * 2 + j, matrix(i, j)); + } else { + EXPECT_EQ(j * 2 + i, matrix(i, j)); + } + } + } + } + + std::unique_ptr runner_; +}; + +TEST_F(VectorIntToTensorCalculatorTest, TestSingleValue) { + SetUpRunner(VectorIntToTensorCalculatorOptions::INPUT_1D, + tensorflow::DT_INT32, false, true); + const int64 time = 1234; + runner_->MutableInputs() + ->Tag("SINGLE_INT") + .packets.push_back(MakePacket(1).At(Timestamp(time))); + + EXPECT_TRUE(runner_->Run().ok()); + + const std::vector& output_packets = + runner_->Outputs().Tag("TENSOR_OUT").packets; + EXPECT_EQ(1, output_packets.size()); + EXPECT_EQ(time, output_packets[0].Timestamp().Value()); + const tf::Tensor& output_tensor = output_packets[0].Get(); + + EXPECT_EQ(1, output_tensor.dims()); + EXPECT_EQ(tf::DT_INT32, output_tensor.dtype()); + const auto vec = output_tensor.vec(); + EXPECT_EQ(1, vec(0)); +} + +TEST_F(VectorIntToTensorCalculatorTest, TesOneDim) { + SetUpRunner(VectorIntToTensorCalculatorOptions::INPUT_1D, + tensorflow::DT_INT32, false, false); + auto input = ::absl::make_unique>(5); + for (int i = 0; i < 5; ++i) { + input->at(i) = i; + } + const int64 time = 1234; + runner_->MutableInputs() + ->Tag("VECTOR_INT") + .packets.push_back(Adopt(input.release()).At(Timestamp(time))); + + EXPECT_TRUE(runner_->Run().ok()); + + const std::vector& output_packets = + runner_->Outputs().Tag("TENSOR_OUT").packets; + EXPECT_EQ(1, output_packets.size()); + EXPECT_EQ(time, output_packets[0].Timestamp().Value()); + const tf::Tensor& output_tensor = output_packets[0].Get(); + + EXPECT_EQ(1, output_tensor.dims()); + EXPECT_EQ(tf::DT_INT32, output_tensor.dtype()); + const auto vec = output_tensor.vec(); + + for (int i = 0; i < 5; ++i) { + EXPECT_EQ(i, vec(i)); + } +} + +TEST_F(VectorIntToTensorCalculatorTest, TestTwoDims) { + for (bool transpose : {false, true}) { + TestConvertFromVectoVectorInt(transpose); + } +} + +TEST_F(VectorIntToTensorCalculatorTest, TestInt64) { + SetUpRunner(VectorIntToTensorCalculatorOptions::INPUT_1D, + tensorflow::DT_INT64, false, true); + const int64 time = 1234; + runner_->MutableInputs() + ->Tag("SINGLE_INT") + .packets.push_back(MakePacket(2 ^ 31).At(Timestamp(time))); + + EXPECT_TRUE(runner_->Run().ok()); + + const std::vector& output_packets = + runner_->Outputs().Tag("TENSOR_OUT").packets; + EXPECT_EQ(1, output_packets.size()); + EXPECT_EQ(time, output_packets[0].Timestamp().Value()); + const tf::Tensor& output_tensor = output_packets[0].Get(); + + EXPECT_EQ(1, output_tensor.dims()); + EXPECT_EQ(tf::DT_INT64, output_tensor.dtype()); + const auto vec = output_tensor.vec(); + EXPECT_EQ(2 ^ 31, vec(0)); +} + +TEST_F(VectorIntToTensorCalculatorTest, TestUint8) { + SetUpRunner(VectorIntToTensorCalculatorOptions::INPUT_1D, + tensorflow::DT_UINT8, false, false); + auto input = ::absl::make_unique>(5); + for (int i = 0; i < 5; ++i) { + input->at(i) = i; + } + const int64 time = 1234; + runner_->MutableInputs() + ->Tag("VECTOR_INT") + .packets.push_back(Adopt(input.release()).At(Timestamp(time))); + + EXPECT_TRUE(runner_->Run().ok()); + + const std::vector& output_packets = + runner_->Outputs().Tag("TENSOR_OUT").packets; + EXPECT_EQ(1, output_packets.size()); + EXPECT_EQ(time, output_packets[0].Timestamp().Value()); + const tf::Tensor& output_tensor = output_packets[0].Get(); + + EXPECT_EQ(1, output_tensor.dims()); + EXPECT_EQ(tf::DT_UINT8, output_tensor.dtype()); + const auto vec = output_tensor.vec(); + + for (int i = 0; i < 5; ++i) { + EXPECT_EQ(i, vec(i)); + } +} + +} // namespace +} // namespace mediapipe diff --git a/mediapipe/calculators/tflite/tflite_converter_calculator.cc b/mediapipe/calculators/tflite/tflite_converter_calculator.cc index 598ae4965..a9dccaed8 100644 --- a/mediapipe/calculators/tflite/tflite_converter_calculator.cc +++ b/mediapipe/calculators/tflite/tflite_converter_calculator.cc @@ -25,7 +25,8 @@ #include "tensorflow/lite/error_reporter.h" #include "tensorflow/lite/interpreter.h" -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) #include "mediapipe/gpu/gl_calculator_helper.h" #include "mediapipe/gpu/gpu_buffer.h" #include "tensorflow/lite/delegates/gpu/gl/gl_buffer.h" @@ -45,7 +46,8 @@ #include "tensorflow/lite/delegates/gpu/metal_delegate.h" #endif // iOS -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) typedef ::tflite::gpu::gl::GlBuffer GpuTensor; #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS typedef id GpuTensor; @@ -67,7 +69,8 @@ typedef Eigen::Matrix namespace mediapipe { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) using ::tflite::gpu::gl::CreateReadWriteShaderStorageBuffer; using ::tflite::gpu::gl::GlProgram; using ::tflite::gpu::gl::GlShader; @@ -146,7 +149,8 @@ class TfLiteConverterCalculator : public CalculatorBase { std::unique_ptr interpreter_ = nullptr; -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) mediapipe::GlCalculatorHelper gpu_helper_; std::unique_ptr gpu_data_out_; #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS @@ -181,7 +185,7 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); if (cc->Inputs().HasTag("IMAGE")) cc->Inputs().Tag("IMAGE").Set(); if (cc->Inputs().HasTag("MATRIX")) cc->Inputs().Tag("MATRIX").Set(); -#if !defined(MEDIAPIPE_DISABLE_GPU) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) if (cc->Inputs().HasTag("IMAGE_GPU")) { cc->Inputs().Tag("IMAGE_GPU").Set(); use_gpu |= true; @@ -190,7 +194,7 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); if (cc->Outputs().HasTag("TENSORS")) cc->Outputs().Tag("TENSORS").Set>(); -#if !defined(MEDIAPIPE_DISABLE_GPU) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) if (cc->Outputs().HasTag("TENSORS_GPU")) { cc->Outputs().Tag("TENSORS_GPU").Set>(); use_gpu |= true; @@ -198,7 +202,8 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); #endif // !MEDIAPIPE_DISABLE_GPU if (use_gpu) { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS MP_RETURN_IF_ERROR([MPPMetalHelper updateContract:cc]); @@ -218,7 +223,7 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); if (cc->Inputs().HasTag("IMAGE_GPU") || cc->Outputs().HasTag("IMAGE_OUT_GPU")) { -#if !defined(MEDIAPIPE_DISABLE_GPU) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) use_gpu_ = true; #else RET_CHECK_FAIL() << "GPU processing not enabled."; @@ -231,7 +236,8 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); cc->Outputs().HasTag("TENSORS_GPU")); // Cannot use quantization. use_quantized_tensors_ = false; -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) MP_RETURN_IF_ERROR(gpu_helper_.Open(cc)); #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS gpu_helper_ = [[MPPMetalHelper alloc] initWithCalculatorContext:cc]; @@ -264,7 +270,8 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); } ::mediapipe::Status TfLiteConverterCalculator::Close(CalculatorContext* cc) { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) gpu_helper_.RunInGlContext([this] { gpu_data_out_.reset(); }); #endif #if defined(__APPLE__) && !TARGET_OS_OSX // iOS @@ -383,7 +390,8 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); ::mediapipe::Status TfLiteConverterCalculator::ProcessGPU( CalculatorContext* cc) { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) // GpuBuffer to tflite::gpu::GlBuffer conversion. const auto& input = cc->Inputs().Tag("IMAGE_GPU").Get(); MP_RETURN_IF_ERROR( @@ -468,7 +476,7 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); } ::mediapipe::Status TfLiteConverterCalculator::InitGpu(CalculatorContext* cc) { -#if !defined(MEDIAPIPE_DISABLE_GPU) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) // Get input image sizes. const auto& input = cc->Inputs().Tag("IMAGE_GPU").Get(); mediapipe::ImageFormat::Format format = @@ -485,7 +493,8 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); RET_CHECK_FAIL() << "Num input channels is less than desired output."; #endif // !MEDIAPIPE_DISABLE_GPU -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext( [this, &include_alpha, &input, &single_channel]() -> ::mediapipe::Status { // Device memory. diff --git a/mediapipe/calculators/tflite/tflite_inference_calculator.cc b/mediapipe/calculators/tflite/tflite_inference_calculator.cc index 9bc02b48c..ebd632df9 100644 --- a/mediapipe/calculators/tflite/tflite_inference_calculator.cc +++ b/mediapipe/calculators/tflite/tflite_inference_calculator.cc @@ -27,7 +27,8 @@ #include "tensorflow/lite/kernels/register.h" #include "tensorflow/lite/model.h" -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) #include "mediapipe/gpu/gl_calculator_helper.h" #include "mediapipe/gpu/gpu_buffer.h" #include "tensorflow/lite/delegates/gpu/common/shape.h" @@ -52,7 +53,8 @@ namespace { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) typedef ::tflite::gpu::gl::GlBuffer GpuTensor; #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS typedef id GpuTensor; @@ -68,13 +70,14 @@ size_t RoundUp(size_t n, size_t m) { return ((n + m - 1) / m) * m; } // NOLINT // * Aux namespace mediapipe { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) using ::tflite::gpu::gl::CopyBuffer; using ::tflite::gpu::gl::CreateReadWriteShaderStorageBuffer; using ::tflite::gpu::gl::GlBuffer; #endif -#if !defined(MEDIAPIPE_DISABLE_GPU) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) struct GPUData { int elements = 1; GpuTensor buffer; @@ -147,7 +150,8 @@ class TfLiteInferenceCalculator : public CalculatorBase { std::unique_ptr model_; TfLiteDelegate* delegate_ = nullptr; -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) mediapipe::GlCalculatorHelper gpu_helper_; std::unique_ptr gpu_data_in_; std::vector> gpu_data_out_; @@ -179,7 +183,7 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); if (cc->Inputs().HasTag("TENSORS")) cc->Inputs().Tag("TENSORS").Set>(); -#if !defined(MEDIAPIPE_DISABLE_GPU) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) if (cc->Inputs().HasTag("TENSORS_GPU")) { cc->Inputs().Tag("TENSORS_GPU").Set>(); use_gpu |= true; @@ -188,7 +192,7 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); if (cc->Outputs().HasTag("TENSORS")) cc->Outputs().Tag("TENSORS").Set>(); -#if !defined(MEDIAPIPE_DISABLE_GPU) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) if (cc->Outputs().HasTag("TENSORS_GPU")) { cc->Outputs().Tag("TENSORS_GPU").Set>(); use_gpu |= true; @@ -206,7 +210,8 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); use_gpu |= options.use_gpu(); if (use_gpu) { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS MP_RETURN_IF_ERROR([MPPMetalHelper updateContract:cc]); @@ -225,7 +230,7 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); MP_RETURN_IF_ERROR(LoadOptions(cc)); if (cc->Inputs().HasTag("TENSORS_GPU")) { -#if !defined(MEDIAPIPE_DISABLE_GPU) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) gpu_input_ = true; gpu_inference_ = true; // Inference must be on GPU also. #else @@ -235,7 +240,7 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); } if (cc->Outputs().HasTag("TENSORS_GPU")) { -#if !defined(MEDIAPIPE_DISABLE_GPU) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) gpu_output_ = true; RET_CHECK(cc->Inputs().HasTag("TENSORS_GPU")) << "GPU output must also have GPU Input."; @@ -248,13 +253,15 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); MP_RETURN_IF_ERROR(LoadModel(cc)); if (gpu_inference_) { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) MP_RETURN_IF_ERROR(gpu_helper_.Open(cc)); #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS gpu_helper_ = [[MPPMetalHelper alloc] initWithCalculatorContext:cc]; RET_CHECK(gpu_helper_); #endif -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext( [this, &cc]() -> ::mediapipe::Status { return LoadDelegate(cc); })); #else @@ -262,6 +269,10 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); #endif } +#if defined(__EMSCRIPTEN__) + MP_RETURN_IF_ERROR(LoadDelegate(cc)); +#endif // __EMSCRIPTEN__ + return ::mediapipe::OkStatus(); } @@ -269,7 +280,8 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); // 1. Receive pre-processed tensor inputs. if (gpu_input_) { // Read GPU input into SSBO. -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) const auto& input_tensors = cc->Inputs().Tag("TENSORS_GPU").Get>(); RET_CHECK_EQ(input_tensors.size(), 1); @@ -315,7 +327,8 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); // 2. Run inference. if (gpu_inference_) { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) MP_RETURN_IF_ERROR( gpu_helper_.RunInGlContext([this]() -> ::mediapipe::Status { RET_CHECK_EQ(interpreter_->Invoke(), kTfLiteOk); @@ -330,7 +343,8 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); // 3. Output processed tensors. if (gpu_output_) { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) // Output result tensors (GPU). auto output_tensors = absl::make_unique>(); MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext( @@ -392,7 +406,8 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); ::mediapipe::Status TfLiteInferenceCalculator::Close(CalculatorContext* cc) { if (delegate_) { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext([this]() -> Status { TfLiteGpuDelegateDelete(delegate_); gpu_data_in_.reset(); @@ -456,6 +471,10 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); RET_CHECK(interpreter_); +#if defined(__EMSCRIPTEN__) + interpreter_->SetNumThreads(1); +#endif // __EMSCRIPTEN__ + if (gpu_output_) { use_quantized_tensors_ = false; } else { @@ -471,7 +490,8 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); ::mediapipe::Status TfLiteInferenceCalculator::LoadDelegate( CalculatorContext* cc) { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) // Configure and create the delegate. TfLiteGpuDelegateOptions options = TfLiteGpuDelegateOptionsDefault(); options.compile_options.precision_loss_allowed = 1; diff --git a/mediapipe/calculators/tflite/tflite_tensors_to_classification_calculator.cc b/mediapipe/calculators/tflite/tflite_tensors_to_classification_calculator.cc index 5e9e9988e..906b4242f 100644 --- a/mediapipe/calculators/tflite/tflite_tensors_to_classification_calculator.cc +++ b/mediapipe/calculators/tflite/tflite_tensors_to_classification_calculator.cc @@ -24,7 +24,8 @@ #include "mediapipe/framework/port/ret_check.h" #include "mediapipe/util/resource_util.h" #include "tensorflow/lite/interpreter.h" -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if defined(__EMSCRIPTEN__) || defined(__ANDROID__) || \ + (defined(__APPLE__) && !TARGET_OS_OSX) #include "mediapipe/util/android/file/base/file.h" #include "mediapipe/util/android/file/base/helpers.h" #else @@ -66,8 +67,8 @@ class TfLiteTensorsToClassificationCalculator : public CalculatorBase { ::mediapipe::Status Close(CalculatorContext* cc) override; private: + ::mediapipe::TfLiteTensorsToClassificationCalculatorOptions options_; int top_k_ = 0; - double min_score_threshold_ = 0; std::unordered_map label_map_; bool label_map_loaded_ = false; }; @@ -93,15 +94,14 @@ REGISTER_CALCULATOR(TfLiteTensorsToClassificationCalculator); CalculatorContext* cc) { cc->SetOffset(TimestampDiff(0)); - auto options = cc->Options< + options_ = cc->Options< ::mediapipe::TfLiteTensorsToClassificationCalculatorOptions>(); - top_k_ = options.top_k(); - min_score_threshold_ = options.min_score_threshold(); - if (options.has_label_map_path()) { + top_k_ = options_.top_k(); + if (options_.has_label_map_path()) { std::string string_path; ASSIGN_OR_RETURN(string_path, - PathToResourceAsFile(options.label_map_path())); + PathToResourceAsFile(options_.label_map_path())); std::string label_map_string; MP_RETURN_IF_ERROR(file::GetContents(string_path, &label_map_string)); @@ -125,9 +125,11 @@ REGISTER_CALCULATOR(TfLiteTensorsToClassificationCalculator); RET_CHECK_EQ(input_tensors.size(), 1); const TfLiteTensor* raw_score_tensor = &input_tensors[0]; - RET_CHECK_EQ(raw_score_tensor->dims->size, 2); - RET_CHECK_EQ(raw_score_tensor->dims->data[0], 1); - int num_classes = raw_score_tensor->dims->data[1]; + int num_classes = 1; + for (int i = 0; i < raw_score_tensor->dims->size; ++i) { + num_classes *= raw_score_tensor->dims->data[i]; + } + if (label_map_loaded_) { RET_CHECK_EQ(num_classes, label_map_.size()); } @@ -135,7 +137,8 @@ REGISTER_CALCULATOR(TfLiteTensorsToClassificationCalculator); auto classification_list = absl::make_unique(); for (int i = 0; i < num_classes; ++i) { - if (raw_scores[i] < min_score_threshold_) { + if (options_.has_min_score_threshold() && + raw_scores[i] < options_.min_score_threshold()) { continue; } Classification* classification = classification_list->add_classification(); @@ -148,6 +151,7 @@ REGISTER_CALCULATOR(TfLiteTensorsToClassificationCalculator); // Note that partial_sort will raise error when top_k_ > // classification_list->classification_size(). + CHECK_GE(classification_list->classification_size(), top_k_); auto raw_classification_list = classification_list->mutable_classification(); if (top_k_ > 0 && classification_list->classification_size() >= top_k_) { std::partial_sort(raw_classification_list->begin(), diff --git a/mediapipe/calculators/tflite/tflite_tensors_to_detections_calculator.cc b/mediapipe/calculators/tflite/tflite_tensors_to_detections_calculator.cc index 8e790b00a..22d8b4d0e 100644 --- a/mediapipe/calculators/tflite/tflite_tensors_to_detections_calculator.cc +++ b/mediapipe/calculators/tflite/tflite_tensors_to_detections_calculator.cc @@ -27,7 +27,8 @@ #include "mediapipe/framework/port/ret_check.h" #include "tensorflow/lite/interpreter.h" -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) #include "mediapipe/gpu/gl_calculator_helper.h" #include "tensorflow/lite/delegates/gpu/gl/gl_buffer.h" #include "tensorflow/lite/delegates/gpu/gl/gl_program.h" @@ -55,12 +56,14 @@ constexpr int kNumCoordsPerBox = 4; namespace mediapipe { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) using ::tflite::gpu::gl::CreateReadWriteShaderStorageBuffer; using ::tflite::gpu::gl::GlShader; #endif -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) typedef ::tflite::gpu::gl::GlBuffer GpuTensor; typedef ::tflite::gpu::gl::GlProgram GpuProgram; #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS @@ -70,7 +73,7 @@ typedef id GpuProgram; namespace { -#if !defined(MEDIAPIPE_DISABLE_GPU) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) struct GPUData { GpuProgram decode_program; GpuProgram score_program; @@ -169,18 +172,21 @@ class TfLiteTensorsToDetectionsCalculator : public CalculatorBase { const int* detection_classes, std::vector* output_detections); Detection ConvertToDetection(float box_ymin, float box_xmin, float box_ymax, float box_xmax, float score, int class_id, - bool flip_vertically); + int detection_id, bool flip_vertically); int num_classes_ = 0; int num_boxes_ = 0; int num_coords_ = 0; + // Unique detection ID per new detection. + static int next_detection_id_; std::set ignore_classes_; ::mediapipe::TfLiteTensorsToDetectionsCalculatorOptions options_; std::vector anchors_; bool side_packet_anchors_{}; -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) mediapipe::GlCalculatorHelper gpu_helper_; std::unique_ptr gpu_data_; #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS @@ -193,6 +199,10 @@ class TfLiteTensorsToDetectionsCalculator : public CalculatorBase { }; REGISTER_CALCULATOR(TfLiteTensorsToDetectionsCalculator); +// Initialization of non-const static member should happen outside class +// definition. +int TfLiteTensorsToDetectionsCalculator::next_detection_id_ = 0; + ::mediapipe::Status TfLiteTensorsToDetectionsCalculator::GetContract( CalculatorContract* cc) { RET_CHECK(!cc->Inputs().GetTags().empty()); @@ -204,7 +214,7 @@ REGISTER_CALCULATOR(TfLiteTensorsToDetectionsCalculator); cc->Inputs().Tag("TENSORS").Set>(); } -#if !defined(MEDIAPIPE_DISABLE_GPU) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) if (cc->Inputs().HasTag("TENSORS_GPU")) { cc->Inputs().Tag("TENSORS_GPU").Set>(); use_gpu |= true; @@ -222,7 +232,8 @@ REGISTER_CALCULATOR(TfLiteTensorsToDetectionsCalculator); } if (use_gpu) { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS MP_RETURN_IF_ERROR([MPPMetalHelper updateContract:cc]); @@ -238,7 +249,8 @@ REGISTER_CALCULATOR(TfLiteTensorsToDetectionsCalculator); if (cc->Inputs().HasTag("TENSORS_GPU")) { gpu_input_ = true; -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) MP_RETURN_IF_ERROR(gpu_helper_.Open(cc)); #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS gpu_helper_ = [[MPPMetalHelper alloc] initWithCalculatorContext:cc]; @@ -400,7 +412,8 @@ REGISTER_CALCULATOR(TfLiteTensorsToDetectionsCalculator); } ::mediapipe::Status TfLiteTensorsToDetectionsCalculator::ProcessGPU( CalculatorContext* cc, std::vector* output_detections) { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) const auto& input_tensors = cc->Inputs().Tag("TENSORS_GPU").Get>(); RET_CHECK_GE(input_tensors.size(), 2); @@ -562,7 +575,8 @@ REGISTER_CALCULATOR(TfLiteTensorsToDetectionsCalculator); ::mediapipe::Status TfLiteTensorsToDetectionsCalculator::Close( CalculatorContext* cc) { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) gpu_helper_.RunInGlContext([this] { gpu_data_.reset(); }); #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS gpu_data_.reset(); @@ -672,7 +686,10 @@ REGISTER_CALCULATOR(TfLiteTensorsToDetectionsCalculator); Detection detection = ConvertToDetection( detection_boxes[box_offset + 0], detection_boxes[box_offset + 1], detection_boxes[box_offset + 2], detection_boxes[box_offset + 3], - detection_scores[i], detection_classes[i], options_.flip_vertically()); + detection_scores[i], detection_classes[i], next_detection_id_, + options_.flip_vertically()); + // Increment to get next unique detection ID. + ++next_detection_id_; // Add keypoints. if (options_.num_keypoints() > 0) { auto* location_data = detection.mutable_location_data(); @@ -695,10 +712,11 @@ REGISTER_CALCULATOR(TfLiteTensorsToDetectionsCalculator); Detection TfLiteTensorsToDetectionsCalculator::ConvertToDetection( float box_ymin, float box_xmin, float box_ymax, float box_xmax, float score, - int class_id, bool flip_vertically) { + int class_id, int detection_id, bool flip_vertically) { Detection detection; detection.add_score(score); detection.add_label_id(class_id); + detection.set_detection_id(detection_id); LocationData* location_data = detection.mutable_location_data(); location_data->set_format(LocationData::RELATIVE_BOUNDING_BOX); @@ -715,7 +733,8 @@ Detection TfLiteTensorsToDetectionsCalculator::ConvertToDetection( ::mediapipe::Status TfLiteTensorsToDetectionsCalculator::GpuInit( CalculatorContext* cc) { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext([this]() -> ::mediapipe::Status { gpu_data_ = absl::make_unique(); diff --git a/mediapipe/calculators/tflite/tflite_tensors_to_landmarks_calculator.cc b/mediapipe/calculators/tflite/tflite_tensors_to_landmarks_calculator.cc index 1d646e4a3..996b1fa35 100644 --- a/mediapipe/calculators/tflite/tflite_tensors_to_landmarks_calculator.cc +++ b/mediapipe/calculators/tflite/tflite_tensors_to_landmarks_calculator.cc @@ -21,7 +21,8 @@ namespace mediapipe { // A calculator for converting TFLite tensors from regression models into -// landmarks. +// landmarks. Note that if the landmarks in the tensor has more than 3 +// dimensions, only the first 3 dimensions will be converted to x,y,z. // // Input: // TENSORS - Vector of TfLiteTensor of type kTfLiteFloat32. Only the first @@ -122,9 +123,6 @@ REGISTER_CALCULATOR(TfLiteTensorsToLandmarksCalculator); num_values *= raw_tensor->dims->data[i]; } const int num_dimensions = num_values / num_landmarks_; - // Landmarks must have less than 3 dimensions. Otherwise please consider - // using matrix. - CHECK_LE(num_dimensions, 3); CHECK_GT(num_dimensions, 0); const float* raw_landmarks = raw_tensor->data.f; diff --git a/mediapipe/calculators/tflite/tflite_tensors_to_segmentation_calculator.cc b/mediapipe/calculators/tflite/tflite_tensors_to_segmentation_calculator.cc index 16805a066..55279308a 100644 --- a/mediapipe/calculators/tflite/tflite_tensors_to_segmentation_calculator.cc +++ b/mediapipe/calculators/tflite/tflite_tensors_to_segmentation_calculator.cc @@ -28,7 +28,8 @@ #include "mediapipe/util/resource_util.h" #include "tensorflow/lite/interpreter.h" -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) #include "mediapipe/gpu/gl_calculator_helper.h" #include "mediapipe/gpu/gl_simple_shaders.h" #include "mediapipe/gpu/shader_util.h" @@ -53,7 +54,8 @@ float Clamp(float val, float min, float max) { namespace mediapipe { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) using ::tflite::gpu::gl::CopyBuffer; using ::tflite::gpu::gl::CreateReadWriteRgbaImageTexture; using ::tflite::gpu::gl::CreateReadWriteShaderStorageBuffer; @@ -129,7 +131,8 @@ class TfLiteTensorsToSegmentationCalculator : public CalculatorBase { int tensor_channels_ = 0; bool use_gpu_ = false; -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) mediapipe::GlCalculatorHelper gpu_helper_; std::unique_ptr mask_program_with_prev_; std::unique_ptr mask_program_no_prev_; @@ -159,7 +162,8 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); } // Inputs GPU. -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) if (cc->Inputs().HasTag("TENSORS_GPU")) { cc->Inputs().Tag("TENSORS_GPU").Set>(); use_gpu |= true; @@ -178,7 +182,8 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); if (cc->Outputs().HasTag("MASK")) { cc->Outputs().Tag("MASK").Set(); } -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) if (cc->Outputs().HasTag("MASK_GPU")) { cc->Outputs().Tag("MASK_GPU").Set(); use_gpu |= true; @@ -186,7 +191,8 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); #endif // !MEDIAPIPE_DISABLE_GPU if (use_gpu) { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); #endif // !MEDIAPIPE_DISABLE_GPU } @@ -199,7 +205,8 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); if (cc->Inputs().HasTag("TENSORS_GPU")) { use_gpu_ = true; -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) MP_RETURN_IF_ERROR(gpu_helper_.Open(cc)); #endif // !MEDIAPIPE_DISABLE_GPU } @@ -207,7 +214,8 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); MP_RETURN_IF_ERROR(LoadOptions(cc)); if (use_gpu_) { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) MP_RETURN_IF_ERROR( gpu_helper_.RunInGlContext([this, cc]() -> ::mediapipe::Status { MP_RETURN_IF_ERROR(InitGpu(cc)); @@ -224,7 +232,8 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); ::mediapipe::Status TfLiteTensorsToSegmentationCalculator::Process( CalculatorContext* cc) { if (use_gpu_) { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) MP_RETURN_IF_ERROR( gpu_helper_.RunInGlContext([this, cc]() -> ::mediapipe::Status { MP_RETURN_IF_ERROR(ProcessGpu(cc)); @@ -240,7 +249,8 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); ::mediapipe::Status TfLiteTensorsToSegmentationCalculator::Close( CalculatorContext* cc) { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) gpu_helper_.RunInGlContext([this] { if (upsample_program_) glDeleteProgram(upsample_program_); upsample_program_ = 0; @@ -367,7 +377,8 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); if (cc->Inputs().Tag("TENSORS_GPU").IsEmpty()) { return ::mediapipe::OkStatus(); } -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) // Get input streams. const auto& input_tensors = cc->Inputs().Tag("TENSORS_GPU").Get>(); @@ -453,7 +464,8 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); } void TfLiteTensorsToSegmentationCalculator::GlRender() { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) static const GLfloat square_vertices[] = { -1.0f, -1.0f, // bottom left 1.0f, -1.0f, // bottom right @@ -525,7 +537,8 @@ void TfLiteTensorsToSegmentationCalculator::GlRender() { ::mediapipe::Status TfLiteTensorsToSegmentationCalculator::InitGpu( CalculatorContext* cc) { -#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__EMSCRIPTEN__) && \ + !defined(__APPLE__) MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext([this]() -> ::mediapipe::Status { // A shader to process a segmentation tensor into an output mask, diff --git a/mediapipe/calculators/util/BUILD b/mediapipe/calculators/util/BUILD index 7bd06fe97..9f3f687b2 100644 --- a/mediapipe/calculators/util/BUILD +++ b/mediapipe/calculators/util/BUILD @@ -14,7 +14,7 @@ licenses(["notice"]) # Apache 2.0 -package(default_visibility = ["//visibility:private"]) +package(default_visibility = ["//visibility:public"]) exports_files(["LICENSE"]) @@ -234,6 +234,7 @@ cc_library( "//mediapipe/framework/port:status", "//mediapipe/framework/port:vector", "//mediapipe/util:annotation_renderer", + "//mediapipe/util:render_data_cc_proto", ] + select({ "//mediapipe/gpu:disable_gpu": [], "//conditions:default": [ @@ -360,6 +361,16 @@ mediapipe_cc_proto_library( deps = [":landmark_projection_calculator_proto"], ) +mediapipe_cc_proto_library( + name = "landmarks_to_floats_calculator_cc_proto", + srcs = ["landmarks_to_floats_calculator.proto"], + cc_deps = [ + "//mediapipe/framework:calculator_cc_proto", + ], + visibility = ["//visibility:public"], + deps = [":landmarks_to_floats_calculator_proto"], +) + mediapipe_cc_proto_library( name = "rect_transformation_calculator_cc_proto", srcs = ["rect_transformation_calculator.proto"], @@ -372,7 +383,12 @@ mediapipe_cc_proto_library( cc_library( name = "detections_to_rects_calculator", - srcs = ["detections_to_rects_calculator.cc"], + srcs = [ + "detections_to_rects_calculator.cc", + ], + hdrs = [ + "detections_to_rects_calculator.h", + ], visibility = ["//visibility:public"], deps = [ ":detections_to_rects_calculator_cc_proto", @@ -454,6 +470,17 @@ proto_library( ], ) +proto_library( + name = "labels_to_render_data_calculator_proto", + srcs = ["labels_to_render_data_calculator.proto"], + visibility = ["//visibility:public"], + deps = [ + "//mediapipe/framework:calculator_proto", + "//mediapipe/util:color_proto", + "//mediapipe/util:render_data_proto", + ], +) + proto_library( name = "thresholding_calculator_proto", srcs = ["thresholding_calculator.proto"], @@ -483,6 +510,15 @@ proto_library( ], ) +proto_library( + name = "landmarks_to_floats_calculator_proto", + srcs = ["landmarks_to_floats_calculator.proto"], + visibility = ["//visibility:public"], + deps = [ + "//mediapipe/framework:calculator_proto", + ], +) + proto_library( name = "rect_transformation_calculator_proto", srcs = ["rect_transformation_calculator.proto"], @@ -577,6 +613,26 @@ cc_library( alwayslink = 1, ) +cc_library( + name = "labels_to_render_data_calculator", + srcs = ["labels_to_render_data_calculator.cc"], + visibility = ["//visibility:public"], + deps = [ + ":labels_to_render_data_calculator_cc_proto", + "//mediapipe/framework:calculator_framework", + "//mediapipe/framework:calculator_options_cc_proto", + "//mediapipe/framework/formats:classification_cc_proto", + "//mediapipe/framework/formats:video_stream_header", + "//mediapipe/framework/port:ret_check", + "//mediapipe/framework/port:status", + "//mediapipe/framework/port:statusor", + "//mediapipe/util:color_cc_proto", + "//mediapipe/util:render_data_cc_proto", + "@com_google_absl//absl/strings", + ], + alwayslink = 1, +) + cc_library( name = "rect_to_render_data_calculator", srcs = ["rect_to_render_data_calculator.cc"], @@ -658,6 +714,22 @@ cc_library( alwayslink = 1, ) +cc_library( + name = "landmarks_to_floats_calculator", + srcs = ["landmarks_to_floats_calculator.cc"], + visibility = ["//visibility:public"], + deps = [ + ":landmarks_to_floats_calculator_cc_proto", + "//mediapipe/framework:calculator_framework", + "//mediapipe/framework/formats:landmark_cc_proto", + "//mediapipe/framework/formats:matrix", + "//mediapipe/framework/port:ret_check", + "//mediapipe/framework/port:status", + "@eigen_archive//:eigen", + ], + alwayslink = 1, +) + cc_test( name = "detection_letterbox_removal_calculator_test", srcs = ["detection_letterbox_removal_calculator_test.cc"], @@ -714,6 +786,7 @@ cc_library( visibility = ["//visibility:public"], deps = [ ":top_k_scores_calculator_cc_proto", + "//mediapipe/framework/formats:classification_cc_proto", "//mediapipe/framework/port:ret_check", "//mediapipe/framework/port:status", "//mediapipe/framework/port:statusor", @@ -750,3 +823,27 @@ cc_test( "//mediapipe/framework/port:status", ], ) + +mediapipe_cc_proto_library( + name = "labels_to_render_data_calculator_cc_proto", + srcs = ["labels_to_render_data_calculator.proto"], + cc_deps = [ + "//mediapipe/framework:calculator_cc_proto", + "//mediapipe/util:color_cc_proto", + ], + visibility = ["//visibility:public"], + deps = [":labels_to_render_data_calculator_proto"], +) + +cc_library( + name = "local_file_contents_calculator", + srcs = ["local_file_contents_calculator.cc"], + visibility = ["//visibility:public"], + deps = [ + "//mediapipe/framework:calculator_framework", + "//mediapipe/framework/port:file_helpers", + "//mediapipe/framework/port:ret_check", + "//mediapipe/framework/port:status", + ], + alwayslink = 1, +) diff --git a/mediapipe/calculators/util/annotation_overlay_calculator.cc b/mediapipe/calculators/util/annotation_overlay_calculator.cc index 5f5c53582..4b8776c3c 100644 --- a/mediapipe/calculators/util/annotation_overlay_calculator.cc +++ b/mediapipe/calculators/util/annotation_overlay_calculator.cc @@ -26,6 +26,7 @@ #include "mediapipe/framework/port/vector.h" #include "mediapipe/util/annotation_renderer.h" #include "mediapipe/util/color.pb.h" +#include "mediapipe/util/render_data.pb.h" #if !defined(MEDIAPIPE_DISABLE_GPU) #include "mediapipe/gpu/gl_calculator_helper.h" @@ -41,6 +42,8 @@ namespace { constexpr char kInputFrameTag[] = "INPUT_FRAME"; constexpr char kOutputFrameTag[] = "OUTPUT_FRAME"; +constexpr char kInputVectorTag[] = "VECTOR"; + constexpr char kInputFrameTagGpu[] = "INPUT_FRAME_GPU"; constexpr char kOutputFrameTagGpu[] = "OUTPUT_FRAME_GPU"; @@ -65,6 +68,9 @@ constexpr int kAnnotationBackgroundColor[] = {100, 101, 102}; // 2. RenderData proto on variable number of input streams. All the RenderData // at a particular timestamp is drawn on the image in the order of their // input streams. No tags required. +// 3. std::vector on variable number of input streams. RenderData +// objects at a particular timestamp are drawn on the image in order of the +// input vector items. These input streams are tagged with "VECTOR". // // Output: // 1. OUTPUT_FRAME or OUTPUT_FRAME_GPU: A rendered ImageFrame (or GpuBuffer). @@ -85,6 +91,8 @@ constexpr int kAnnotationBackgroundColor[] = {100, 101, 102}; // input_stream: "render_data_1" // input_stream: "render_data_2" // input_stream: "render_data_3" +// input_stream: "VECTOR:0:render_data_vec_0" +// input_stream: "VECTOR:1:render_data_vec_1" // output_stream: "OUTPUT_FRAME:decorated_frames" // options { // [mediapipe.AnnotationOverlayCalculatorOptions.ext] { @@ -99,6 +107,8 @@ constexpr int kAnnotationBackgroundColor[] = {100, 101, 102}; // input_stream: "render_data_1" // input_stream: "render_data_2" // input_stream: "render_data_3" +// input_stream: "VECTOR:0:render_data_vec_0" +// input_stream: "VECTOR:1:render_data_vec_1" // output_stream: "OUTPUT_FRAME_GPU:decorated_frames" // options { // [mediapipe.AnnotationOverlayCalculatorOptions.ext] { @@ -188,8 +198,16 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); } // Data streams to render. - for (int i = 0; i < num_render_streams; ++i) { - cc->Inputs().Index(i).Set(); + for (CollectionItemId id = cc->Inputs().BeginId(); id < cc->Inputs().EndId(); + ++id) { + auto tag_and_index = cc->Inputs().TagAndIndexFromId(id); + std::string tag = tag_and_index.first; + if (tag == kInputVectorTag) { + cc->Inputs().Get(id).Set>(); + } else if (tag.empty()) { + // Empty tag defaults to accepting a single object of RenderData type. + cc->Inputs().Get(id).Set(); + } } // Rendered image. @@ -285,12 +303,28 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); renderer_->AdoptImage(image_mat.get()); // Render streams onto render target. - for (int i = 0; i < num_render_streams_; ++i) { - if (cc->Inputs().Index(i).IsEmpty()) { + for (CollectionItemId id = cc->Inputs().BeginId(); id < cc->Inputs().EndId(); + ++id) { + auto tag_and_index = cc->Inputs().TagAndIndexFromId(id); + std::string tag = tag_and_index.first; + if (!tag.empty() && tag != kInputVectorTag) { continue; } - const RenderData& render_data = cc->Inputs().Index(i).Get(); - renderer_->RenderDataOnImage(render_data); + if (cc->Inputs().Get(id).IsEmpty()) { + continue; + } + if (tag.empty()) { + // Empty tag defaults to accepting a single object of RenderData type. + const RenderData& render_data = cc->Inputs().Get(id).Get(); + renderer_->RenderDataOnImage(render_data); + } else { + RET_CHECK_EQ(kInputVectorTag, tag); + const std::vector& render_data_vec = + cc->Inputs().Get(id).Get>(); + for (const RenderData& render_data : render_data_vec) { + renderer_->RenderDataOnImage(render_data); + } + } } if (use_gpu_) { diff --git a/mediapipe/calculators/util/detection_label_id_to_text_calculator.cc b/mediapipe/calculators/util/detection_label_id_to_text_calculator.cc index 0fb2d30b8..7e8beadf1 100644 --- a/mediapipe/calculators/util/detection_label_id_to_text_calculator.cc +++ b/mediapipe/calculators/util/detection_label_id_to_text_calculator.cc @@ -19,8 +19,8 @@ #include "mediapipe/framework/port/status.h" #include "mediapipe/util/resource_util.h" -#if defined(MEDIAPIPE_LITE) || defined(__ANDROID__) || \ - (defined(__APPLE__) && !TARGET_OS_OSX) +#if defined(MEDIAPIPE_LITE) || defined(__EMSCRIPTEN__) || \ + defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) #include "mediapipe/util/android/file/base/file.h" #include "mediapipe/util/android/file/base/helpers.h" #else diff --git a/mediapipe/calculators/util/detections_to_rects_calculator.cc b/mediapipe/calculators/util/detections_to_rects_calculator.cc index bb5ba6d4d..91a400ca1 100644 --- a/mediapipe/calculators/util/detections_to_rects_calculator.cc +++ b/mediapipe/calculators/util/detections_to_rects_calculator.cc @@ -11,6 +11,8 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +#include "mediapipe/calculators/util/detections_to_rects_calculator.h" + #include #include "mediapipe/calculators/util/detections_to_rects_calculator.pb.h" @@ -24,8 +26,6 @@ namespace mediapipe { -using mediapipe::DetectionsToRectsCalculatorOptions; - namespace { constexpr char kDetectionTag[] = "DETECTION"; @@ -36,7 +36,10 @@ constexpr char kNormRectTag[] = "NORM_RECT"; constexpr char kRectsTag[] = "RECTS"; constexpr char kNormRectsTag[] = "NORM_RECTS"; -::mediapipe::Status DetectionToRect(const Detection& detection, Rect* rect) { +} // namespace + +::mediapipe::Status DetectionsToRectsCalculator::DetectionToRect( + const Detection& detection, Rect* rect) { const LocationData location_data = detection.location_data(); RET_CHECK(location_data.format() == LocationData::BOUNDING_BOX) << "Only Detection with formats of BOUNDING_BOX can be converted to Rect"; @@ -48,8 +51,8 @@ constexpr char kNormRectsTag[] = "NORM_RECTS"; return ::mediapipe::OkStatus(); } -::mediapipe::Status DetectionToNormalizedRect(const Detection& detection, - NormalizedRect* rect) { +::mediapipe::Status DetectionsToRectsCalculator::DetectionToNormalizedRect( + const Detection& detection, NormalizedRect* rect) { const LocationData location_data = detection.location_data(); RET_CHECK(location_data.format() == LocationData::RELATIVE_BOUNDING_BOX) << "Only Detection with formats of RELATIVE_BOUNDING_BOX can be " @@ -63,79 +66,6 @@ constexpr char kNormRectsTag[] = "NORM_RECTS"; return ::mediapipe::OkStatus(); } -// Wraps around an angle in radians to within -M_PI and M_PI. -inline float NormalizeRadians(float angle) { - return angle - 2 * M_PI * std::floor((angle - (-M_PI)) / (2 * M_PI)); -} - -} // namespace - -// A calculator that converts Detection proto to Rect proto. -// -// Detection is the format for encoding one or more detections in an image. -// The input can be a single Detection or std::vector. The output can -// be either a single Rect or NormalizedRect, or std::vector or -// std::vector. If Rect is used, the LocationData format is -// expected to be BOUNDING_BOX, and if NormalizedRect is used it is expected to -// be RELATIVE_BOUNDING_BOX. -// -// When the input is std::vector and the output is a Rect or -// NormalizedRect, only the first detection is converted. When the input is a -// single Detection and the output is a std::vector or -// std::vector, the output is a vector of size 1. -// -// Inputs: -// -// One of the following: -// DETECTION: A Detection proto. -// DETECTIONS: An std::vector. -// -// IMAGE_SIZE (optional): A std::pair represention image width and -// height. This is required only when rotation needs to be computed (see -// calculator options). -// -// Output: -// One of the following: -// RECT: A Rect proto. -// NORM_RECT: A NormalizedRect proto. -// RECTS: An std::vector. -// NORM_RECTS: An std::vector. -// -// Example config: -// node { -// calculator: "DetectionsToRectsCalculator" -// input_stream: "DETECTIONS:detections" -// input_stream: "IMAGE_SIZE:image_size" -// output_stream: "NORM_RECT:rect" -// options: { -// [mediapipe.DetectionsToRectCalculatorOptions.ext] { -// rotation_vector_start_keypoint_index: 0 -// rotation_vector_end_keypoint_index: 2 -// rotation_vector_target_angle_degrees: 90 -// output_zero_rect_for_empty_detections: true -// } -// } -// } -class DetectionsToRectsCalculator : public CalculatorBase { - public: - static ::mediapipe::Status GetContract(CalculatorContract* cc); - - ::mediapipe::Status Open(CalculatorContext* cc) override; - ::mediapipe::Status Process(CalculatorContext* cc) override; - - private: - float ComputeRotation(const Detection& detection, - const std::pair image_size); - - DetectionsToRectsCalculatorOptions options_; - int start_keypoint_index_; - int end_keypoint_index_; - float target_angle_; // In radians. - bool rotate_; - bool output_zero_rect_for_empty_detections_; -}; -REGISTER_CALCULATOR(DetectionsToRectsCalculator); - ::mediapipe::Status DetectionsToRectsCalculator::GetContract( CalculatorContract* cc) { RET_CHECK(cc->Inputs().HasTag(kDetectionTag) ^ @@ -232,6 +162,13 @@ REGISTER_CALCULATOR(DetectionsToRectsCalculator); .Tag(kNormRectTag) .AddPacket(MakePacket().At(cc->InputTimestamp())); } + if (cc->Outputs().HasTag(kNormRectsTag)) { + auto rect_vector = absl::make_unique>(); + rect_vector->emplace_back(NormalizedRect()); + cc->Outputs() + .Tag(kNormRectsTag) + .Add(rect_vector.release(), cc->InputTimestamp()); + } } return ::mediapipe::OkStatus(); } @@ -312,4 +249,6 @@ float DetectionsToRectsCalculator::ComputeRotation( return NormalizeRadians(rotation); } +REGISTER_CALCULATOR(DetectionsToRectsCalculator); + } // namespace mediapipe diff --git a/mediapipe/calculators/util/detections_to_rects_calculator.h b/mediapipe/calculators/util/detections_to_rects_calculator.h new file mode 100644 index 000000000..82b9f7bcc --- /dev/null +++ b/mediapipe/calculators/util/detections_to_rects_calculator.h @@ -0,0 +1,105 @@ +// Copyright 2019 The MediaPipe Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#ifndef MEDIAPIPE_CALCULATORS_UTIL_DETECTIONS_TO_RECTS_CALCULATOR_H_ +#define MEDIAPIPE_CALCULATORS_UTIL_DETECTIONS_TO_RECTS_CALCULATOR_H_ + +#include + +#include "mediapipe/calculators/util/detections_to_rects_calculator.pb.h" +#include "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/calculator_options.pb.h" +#include "mediapipe/framework/formats/detection.pb.h" +#include "mediapipe/framework/formats/location_data.pb.h" +#include "mediapipe/framework/formats/rect.pb.h" +#include "mediapipe/framework/port/ret_check.h" +#include "mediapipe/framework/port/status.h" + +namespace mediapipe { + +// A calculator that converts Detection proto to Rect proto. +// +// Detection is the format for encoding one or more detections in an image. +// The input can be a single Detection or std::vector. The output can +// be either a single Rect or NormalizedRect, or std::vector or +// std::vector. If Rect is used, the LocationData format is +// expected to be BOUNDING_BOX, and if NormalizedRect is used it is expected to +// be RELATIVE_BOUNDING_BOX. +// +// When the input is std::vector and the output is a Rect or +// NormalizedRect, only the first detection is converted. When the input is a +// single Detection and the output is a std::vector or +// std::vector, the output is a vector of size 1. +// +// Inputs: +// +// One of the following: +// DETECTION: A Detection proto. +// DETECTIONS: An std::vector. +// +// IMAGE_SIZE (optional): A std::pair represention image width and +// height. This is required only when rotation needs to be computed (see +// calculator options). +// +// Output: +// One of the following: +// RECT: A Rect proto. +// NORM_RECT: A NormalizedRect proto. +// RECTS: An std::vector. +// NORM_RECTS: An std::vector. +// +// Example config: +// node { +// calculator: "DetectionsToRectsCalculator" +// input_stream: "DETECTIONS:detections" +// input_stream: "IMAGE_SIZE:image_size" +// output_stream: "NORM_RECT:rect" +// options: { +// [mediapipe.DetectionsToRectCalculatorOptions.ext] { +// rotation_vector_start_keypoint_index: 0 +// rotation_vector_end_keypoint_index: 2 +// rotation_vector_target_angle_degrees: 90 +// output_zero_rect_for_empty_detections: true +// } +// } +// } +class DetectionsToRectsCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc); + + ::mediapipe::Status Open(CalculatorContext* cc) override; + ::mediapipe::Status Process(CalculatorContext* cc) override; + + protected: + virtual float ComputeRotation(const ::mediapipe::Detection& detection, + const std::pair image_size); + virtual ::mediapipe::Status DetectionToRect( + const ::mediapipe::Detection& detection, ::mediapipe::Rect* rect); + virtual ::mediapipe::Status DetectionToNormalizedRect( + const ::mediapipe::Detection& detection, + ::mediapipe::NormalizedRect* rect); + + static inline float NormalizeRadians(float angle) { + return angle - 2 * M_PI * std::floor((angle - (-M_PI)) / (2 * M_PI)); + } + + ::mediapipe::DetectionsToRectsCalculatorOptions options_; + int start_keypoint_index_; + int end_keypoint_index_; + float target_angle_ = 0.0f; // In radians. + bool rotate_; + bool output_zero_rect_for_empty_detections_; +}; + +} // namespace mediapipe +#endif // MEDIAPIPE_CALCULATORS_UTIL_DETECTIONS_TO_RECTS_CALCULATOR_H_ diff --git a/mediapipe/calculators/util/labels_to_render_data_calculator.cc b/mediapipe/calculators/util/labels_to_render_data_calculator.cc new file mode 100644 index 000000000..a7f517291 --- /dev/null +++ b/mediapipe/calculators/util/labels_to_render_data_calculator.cc @@ -0,0 +1,181 @@ +// Copyright 2019 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 + +#include "absl/strings/str_cat.h" +#include "mediapipe/calculators/util/labels_to_render_data_calculator.pb.h" +#include "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/formats/classification.pb.h" +#include "mediapipe/framework/formats/video_stream_header.h" +#include "mediapipe/framework/port/ret_check.h" +#include "mediapipe/framework/port/status.h" +#include "mediapipe/framework/port/statusor.h" +#include "mediapipe/util/color.pb.h" +#include "mediapipe/util/render_data.pb.h" + +namespace mediapipe { + +constexpr float kFontHeightScale = 1.25f; + +// A calculator takes in pairs of labels and scores or classifications, outputs +// generates render data. Either both "LABELS" and "SCORES" or "CLASSIFICATIONS" +// must be present. +// +// Usage example: +// node { +// calculator: "LabelsToRenderDataCalculator" +// input_stream: "LABELS:labels" +// input_stream: "SCORES:scores" +// output_stream: "VIDEO_PRESTREAM:video_header" +// options { +// [LabelsToRenderDataCalculatorOptions.ext] { +// color { r: 255 g: 0 b: 0 } +// color { r: 0 g: 255 b: 0 } +// color { r: 0 g: 0 b: 255 } +// thickness: 2.0 +// font_height_px: 20 +// max_num_labels: 3 +// font_face: 1 +// location: TOP_LEFT +// } +// } +// } +class LabelsToRenderDataCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc); + ::mediapipe::Status Open(CalculatorContext* cc) override; + ::mediapipe::Status Process(CalculatorContext* cc) override; + + private: + LabelsToRenderDataCalculatorOptions options_; + int num_colors_ = 0; + int video_width_ = 0; + int video_height_ = 0; + int label_height_px_ = 0; + int label_left_px_ = 0; +}; +REGISTER_CALCULATOR(LabelsToRenderDataCalculator); + +::mediapipe::Status LabelsToRenderDataCalculator::GetContract( + CalculatorContract* cc) { + if (cc->Inputs().HasTag("CLASSIFICATIONS")) { + cc->Inputs().Tag("CLASSIFICATIONS").Set(); + } else { + RET_CHECK(cc->Inputs().HasTag("LABELS")) + << "Must provide input stream \"LABELS\""; + cc->Inputs().Tag("LABELS").Set>(); + if (cc->Inputs().HasTag("SCORES")) { + cc->Inputs().Tag("SCORES").Set>(); + } + } + if (cc->Inputs().HasTag("VIDEO_PRESTREAM")) { + cc->Inputs().Tag("VIDEO_PRESTREAM").Set(); + } + cc->Outputs().Tag("RENDER_DATA").Set(); + return ::mediapipe::OkStatus(); +} + +::mediapipe::Status LabelsToRenderDataCalculator::Open(CalculatorContext* cc) { + options_ = cc->Options(); + num_colors_ = options_.color_size(); + label_height_px_ = std::ceil(options_.font_height_px() * kFontHeightScale); + return ::mediapipe::OkStatus(); +} + +::mediapipe::Status LabelsToRenderDataCalculator::Process( + CalculatorContext* cc) { + if (cc->Inputs().HasTag("VIDEO_PRESTREAM") && + cc->InputTimestamp() == Timestamp::PreStream()) { + const VideoHeader& video_header = + cc->Inputs().Tag("VIDEO_PRESTREAM").Get(); + video_width_ = video_header.width; + video_height_ = video_header.height; + return ::mediapipe::OkStatus(); + } else { + CHECK_EQ(options_.location(), LabelsToRenderDataCalculatorOptions::TOP_LEFT) + << "Only TOP_LEFT is supported without VIDEO_PRESTREAM."; + } + + std::vector labels; + std::vector scores; + if (cc->Inputs().HasTag("CLASSIFICATIONS")) { + const ClassificationList& classifications = + cc->Inputs().Tag("CLASSIFICATIONS").Get(); + labels.resize(classifications.classification_size()); + scores.resize(classifications.classification_size()); + for (int i = 0; i < classifications.classification_size(); ++i) { + labels[i] = classifications.classification(i).label(); + scores[i] = classifications.classification(i).score(); + } + } else { + const std::vector& label_vector = + cc->Inputs().Tag("LABELS").Get>(); + std::vector score_vector; + if (cc->Inputs().HasTag("SCORES")) { + score_vector = cc->Inputs().Tag("SCORES").Get>(); + } + CHECK_EQ(label_vector.size(), score_vector.size()); + labels.resize(label_vector.size()); + scores.resize(label_vector.size()); + for (int i = 0; i < label_vector.size(); ++i) { + labels[i] = label_vector[i]; + scores[i] = score_vector[i]; + } + } + + RenderData render_data; + int num_label = std::min((int)labels.size(), options_.max_num_labels()); + int label_baseline_px = options_.vertical_offset_px(); + if (options_.location() == LabelsToRenderDataCalculatorOptions::TOP_LEFT) { + label_baseline_px += label_height_px_; + } else if (options_.location() == + LabelsToRenderDataCalculatorOptions::BOTTOM_LEFT) { + label_baseline_px += video_height_ - label_height_px_ * (num_label - 1); + } + label_left_px_ = options_.horizontal_offset_px(); + for (int i = 0; i < num_label; ++i) { + auto* label_annotation = render_data.add_render_annotations(); + label_annotation->set_thickness(options_.thickness()); + if (num_colors_ > 0) { + *(label_annotation->mutable_color()) = options_.color(i % num_colors_); + } else { + label_annotation->mutable_color()->set_r(255); + label_annotation->mutable_color()->set_g(0); + label_annotation->mutable_color()->set_b(0); + } + + auto* text = label_annotation->mutable_text(); + std::string display_text = labels[i]; + if (cc->Inputs().HasTag("SCORES")) { + absl::StrAppend(&display_text, ":", scores[i]); + } + text->set_display_text(display_text); + text->set_font_height(options_.font_height_px()); + text->set_left(label_left_px_); + text->set_baseline(label_baseline_px + i * label_height_px_); + text->set_font_face(options_.font_face()); + } + cc->Outputs() + .Tag("RENDER_DATA") + .AddPacket(MakePacket(render_data).At(cc->InputTimestamp())); + + return ::mediapipe::OkStatus(); +} +} // namespace mediapipe diff --git a/mediapipe/calculators/util/labels_to_render_data_calculator.proto b/mediapipe/calculators/util/labels_to_render_data_calculator.proto new file mode 100644 index 000000000..cd98934a5 --- /dev/null +++ b/mediapipe/calculators/util/labels_to_render_data_calculator.proto @@ -0,0 +1,62 @@ +// Copyright 2019 The MediaPipe Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package mediapipe; + +import "mediapipe/framework/calculator.proto"; +import "mediapipe/util/color.proto"; + +message LabelsToRenderDataCalculatorOptions { + extend CalculatorOptions { + optional LabelsToRenderDataCalculatorOptions ext = 271660364; + } + + // Colors for drawing the label(s). + repeated Color color = 1; + + // Thickness for drawing the label(s). + optional double thickness = 2 [default = 2]; + + // The font height in absolute pixels. + optional int32 font_height_px = 3 [default = 50]; + + // The offset of the starting text in horizontal direction in absolute pixels. + optional int32 horizontal_offset_px = 7 [default = 0]; + // The offset of the starting text in vertical direction in absolute pixels. + optional int32 vertical_offset_px = 8 [default = 0]; + + // The maximum number of labels to display. + optional int32 max_num_labels = 4 [default = 1]; + + // Specifies the font for the text. Font must be one of the following from + // OpenCV: + // cv::FONT_HERSHEY_SIMPLEX (0) + // cv::FONT_HERSHEY_PLAIN (1) + // cv::FONT_HERSHEY_DUPLEX (2) + // cv::FONT_HERSHEY_COMPLEX (3) + // cv::FONT_HERSHEY_TRIPLEX (4) + // cv::FONT_HERSHEY_COMPLEX_SMALL (5) + // cv::FONT_HERSHEY_SCRIPT_SIMPLEX (6) + // cv::FONT_HERSHEY_SCRIPT_COMPLEX (7) + optional int32 font_face = 5 [default = 0]; + + // Label location. + enum Location { + TOP_LEFT = 0; + BOTTOM_LEFT = 1; + } + optional Location location = 6 [default = TOP_LEFT]; +} diff --git a/mediapipe/calculators/util/landmarks_to_floats_calculator.cc b/mediapipe/calculators/util/landmarks_to_floats_calculator.cc new file mode 100644 index 000000000..09ab4b575 --- /dev/null +++ b/mediapipe/calculators/util/landmarks_to_floats_calculator.cc @@ -0,0 +1,138 @@ +// Copyright 2019 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. + +// Copyright 2019 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 "Eigen/Core" +#include "mediapipe/calculators/util/landmarks_to_floats_calculator.pb.h" +#include "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/formats/landmark.pb.h" +#include "mediapipe/framework/formats/matrix.h" +#include "mediapipe/framework/port/ret_check.h" + +namespace mediapipe { + +namespace { + +constexpr char kLandmarksTag[] = "NORM_LANDMARKS"; +constexpr char kFloatsTag[] = "FLOATS"; +constexpr char kMatrixTag[] = "MATRIX"; + +} // namespace + +// Converts a vector of landmarks to a vector of floats or a matrix. +// Input: +// NORM_LANDMARKS: An std::vector. +// +// Output: +// FLOATS(optional): A vector of floats from flattened landmarks. +// MATRIX(optional): A matrix of floats of the landmarks. +// +// Usage example: +// node { +// calculator: "LandmarksToFloatsCalculator" +// input_stream: "NORM_LANDMARKS:landmarks" +// output_stream: "MATRIX:landmark_matrix" +// } +class LandmarksToFloatsCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->Inputs().Tag(kLandmarksTag).Set>(); + RET_CHECK(cc->Outputs().HasTag(kFloatsTag) || + cc->Outputs().HasTag(kMatrixTag)); + if (cc->Outputs().HasTag(kFloatsTag)) { + cc->Outputs().Tag(kFloatsTag).Set>(); + } + if (cc->Outputs().HasTag(kMatrixTag)) { + cc->Outputs().Tag(kMatrixTag).Set(); + } + + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Open(CalculatorContext* cc) override { + cc->SetOffset(TimestampDiff(0)); + const auto& options = + cc->Options<::mediapipe::LandmarksToFloatsCalculatorOptions>(); + num_dimensions_ = options.num_dimensions(); + // Currently number of dimensions must be within [1, 3]. + RET_CHECK_GE(num_dimensions_, 1); + RET_CHECK_LE(num_dimensions_, 3); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) override { + // Only process if there's input landmarks. + if (cc->Inputs().Tag(kLandmarksTag).IsEmpty()) { + return ::mediapipe::OkStatus(); + } + + const auto& input_landmarks = + cc->Inputs().Tag(kLandmarksTag).Get>(); + + if (cc->Outputs().HasTag(kFloatsTag)) { + auto output_floats = absl::make_unique>(); + for (const auto& landmark : input_landmarks) { + output_floats->emplace_back(landmark.x()); + if (num_dimensions_ > 1) { + output_floats->emplace_back(landmark.y()); + } + if (num_dimensions_ > 2) { + output_floats->emplace_back(landmark.z()); + } + } + + cc->Outputs() + .Tag(kFloatsTag) + .Add(output_floats.release(), cc->InputTimestamp()); + } else { + auto output_matrix = absl::make_unique(); + output_matrix->setZero(num_dimensions_, input_landmarks.size()); + for (int i = 0; i < input_landmarks.size(); ++i) { + (*output_matrix)(0, i) = input_landmarks[i].x(); + if (num_dimensions_ > 1) { + (*output_matrix)(1, i) = input_landmarks[i].y(); + } + if (num_dimensions_ > 2) { + (*output_matrix)(2, i) = input_landmarks[i].z(); + } + } + cc->Outputs() + .Tag(kMatrixTag) + .Add(output_matrix.release(), cc->InputTimestamp()); + } + return ::mediapipe::OkStatus(); + } + + private: + int num_dimensions_ = 0; +}; +REGISTER_CALCULATOR(LandmarksToFloatsCalculator); + +} // namespace mediapipe diff --git a/mediapipe/calculators/util/landmarks_to_floats_calculator.proto b/mediapipe/calculators/util/landmarks_to_floats_calculator.proto new file mode 100644 index 000000000..310251e75 --- /dev/null +++ b/mediapipe/calculators/util/landmarks_to_floats_calculator.proto @@ -0,0 +1,28 @@ +// Copyright 2019 The MediaPipe Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package mediapipe; + +import "mediapipe/framework/calculator.proto"; + +message LandmarksToFloatsCalculatorOptions { + extend CalculatorOptions { + optional LandmarksToFloatsCalculatorOptions ext = 274035660; + } + + // Number of dimensions to convert. Must within [1, 3]. + optional int32 num_dimensions = 1 [default = 2]; +} diff --git a/mediapipe/calculators/util/local_file_contents_calculator.cc b/mediapipe/calculators/util/local_file_contents_calculator.cc new file mode 100644 index 000000000..9f8d17724 --- /dev/null +++ b/mediapipe/calculators/util/local_file_contents_calculator.cc @@ -0,0 +1,57 @@ +// Copyright 2019 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 "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/port/file_helpers.h" +#include "mediapipe/framework/port/status.h" + +namespace mediapipe { +// The calculator takes the path to the local file as an input side packet and +// outputs the contents of that file. +// +// Example config: +// node { +// calculator: "LocalFileContentsCalculator" +// input_side_packet: "FILE_PATH:file_path" +// output_side_packet: "CONTENTS:contents" +// } +class LocalFileContentsCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->InputSidePackets().Tag("FILE_PATH").Set(); + cc->OutputSidePackets().Tag("CONTENTS").Set(); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Open(CalculatorContext* cc) override { + std::string contents; + MP_RETURN_IF_ERROR(mediapipe::file::GetContents( + cc->InputSidePackets().Tag("FILE_PATH").Get(), &contents)); + cc->OutputSidePackets() + .Tag("CONTENTS") + .Set(MakePacket(std::move(contents))); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) override { + return ::mediapipe::OkStatus(); + } +}; + +REGISTER_CALCULATOR(LocalFileContentsCalculator); + +} // namespace mediapipe diff --git a/mediapipe/calculators/util/top_k_scores_calculator.cc b/mediapipe/calculators/util/top_k_scores_calculator.cc index 18f2eec62..bc8d30f87 100644 --- a/mediapipe/calculators/util/top_k_scores_calculator.cc +++ b/mediapipe/calculators/util/top_k_scores_calculator.cc @@ -23,13 +23,14 @@ #include "mediapipe/calculators/util/top_k_scores_calculator.pb.h" #include "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/formats/classification.pb.h" #include "mediapipe/framework/port/ret_check.h" #include "mediapipe/framework/port/status.h" #include "mediapipe/framework/port/statusor.h" #include "mediapipe/util/resource_util.h" -#if defined(MEDIAPIPE_LITE) || defined(__ANDROID__) || \ - (defined(__APPLE__) && !TARGET_OS_OSX) +#if defined(MEDIAPIPE_LITE) || defined(__EMSCRIPTEN__) || \ + defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) #include "mediapipe/util/android/file/base/file.h" #include "mediapipe/util/android/file/base/helpers.h" #else @@ -37,8 +38,10 @@ #endif namespace mediapipe { + // A calculator that takes a vector of scores and returns the indexes, scores, -// labels of the top k elements. +// labels of the top k elements, classification protos, and summary std::string +// (in csv format). // // Usage example: // node { @@ -47,6 +50,8 @@ namespace mediapipe { // output_stream: "TOP_K_INDEXES:top_k_indexes" // output_stream: "TOP_K_SCORES:top_k_scores" // output_stream: "TOP_K_LABELS:top_k_labels" +// output_stream: "TOP_K_CLASSIFICATIONS:top_k_classes" +// output_stream: "SUMMARY:summary" // options: { // [mediapipe.TopKScoresCalculatorOptions.ext] { // top_k: 5 @@ -69,6 +74,7 @@ class TopKScoresCalculator : public CalculatorBase { int top_k_ = -1; float threshold_ = 0.0; std::unordered_map label_map_; + bool label_map_loaded_ = false; }; REGISTER_CALCULATOR(TopKScoresCalculator); @@ -84,6 +90,12 @@ REGISTER_CALCULATOR(TopKScoresCalculator); if (cc->Outputs().HasTag("TOP_K_LABELS")) { cc->Outputs().Tag("TOP_K_LABELS").Set>(); } + if (cc->Outputs().HasTag("CLASSIFICATIONS")) { + cc->Outputs().Tag("CLASSIFICATIONS").Set(); + } + if (cc->Outputs().HasTag("SUMMARY")) { + cc->Outputs().Tag("SUMMARY").Set(); + } return ::mediapipe::OkStatus(); } @@ -149,7 +161,7 @@ REGISTER_CALCULATOR(TopKScoresCalculator); reverse(top_k_indexes.begin(), top_k_indexes.end()); reverse(top_k_scores.begin(), top_k_scores.end()); - if (cc->Outputs().HasTag("TOP_K_LABELS")) { + if (label_map_loaded_) { for (int index : top_k_indexes) { top_k_labels.push_back(label_map_[index]); } @@ -172,6 +184,35 @@ REGISTER_CALCULATOR(TopKScoresCalculator); .AddPacket(MakePacket>(top_k_labels) .At(cc->InputTimestamp())); } + + if (cc->Outputs().HasTag("SUMMARY")) { + std::vector results; + for (int index = 0; index < top_k_indexes.size(); ++index) { + if (label_map_loaded_) { + results.push_back( + absl::StrCat(top_k_labels[index], ":", top_k_scores[index])); + } else { + results.push_back( + absl::StrCat(top_k_indexes[index], ":", top_k_scores[index])); + } + } + cc->Outputs().Tag("SUMMARY").AddPacket( + MakePacket(absl::StrJoin(results, ",")) + .At(cc->InputTimestamp())); + } + + if (cc->Outputs().HasTag("TOP_K_CLASSIFICATION")) { + auto classification_list = absl::make_unique(); + for (int index = 0; index < top_k_indexes.size(); ++index) { + Classification* classification = + classification_list->add_classification(); + classification->set_index(top_k_indexes[index]); + classification->set_score(top_k_scores[index]); + if (label_map_loaded_) { + classification->set_label(top_k_labels[index]); + } + } + } return ::mediapipe::OkStatus(); } @@ -188,6 +229,7 @@ REGISTER_CALCULATOR(TopKScoresCalculator); while (std::getline(stream, line)) { label_map_[i++] = line; } + label_map_loaded_ = true; return ::mediapipe::OkStatus(); } diff --git a/mediapipe/docs/android_archive_library.md b/mediapipe/docs/android_archive_library.md new file mode 100644 index 000000000..8c7c42b91 --- /dev/null +++ b/mediapipe/docs/android_archive_library.md @@ -0,0 +1,130 @@ +## MediaPipe Android Archive Library + +***Experimental Only*** + +The MediaPipe Android archive library is a convenient way to use MediaPipe with +Android Studio and Gradle. MediaPipe doesn't publish a general AAR that can be +used by all projects. Instead, developers need to add a mediapipe_aar() target +to generate a custom AAR file for their own projects. This is necessary in order +to include specific resources such as MediaPipe calculators needed for each +project. + +### Steps to build a MediaPipe AAR + +1. Create a mediapipe_aar() target. + + In the MediaPipe directory, create a new mediapipe_aar() target in a BUILD + file. You need to figure out what calculators are used in the graph and + provide the calculator dependencies to the mediapipe_aar(). For example, to + build an AAR for [face detection gpu](./face_detection_mobile_gpu.md), you + can put the following code into + mediapipe/examples/android/src/java/com/google/mediapipe/apps/aar_example/BUILD. + + ``` + load("//mediapipe/java/com/google/mediapipe:mediapipe_aar.bzl", "mediapipe_aar") + + mediapipe_aar( + name = "mp_face_detection_aar", + calculators = ["//mediapipe/graphs/face_detection:mobile_calculators"], + ) + ``` + +2. Run the Bazel build command to generate the AAR. + + ```bash + bazel build -c opt --fat_apk_cpu=arm64-v8a,armeabi-v7a //path/to/the/aar/build/file:aar_name + ``` + + For the face detection AAR target we made in the step 1, run: + + ```bash + bazel build -c opt --fat_apk_cpu=arm64-v8a,armeabi-v7a \ + //mediapipe/examples/android/src/java/com/google/mediapipe/apps/aar_example:mp_face_detection_aar + + # It should print: + # Target //mediapipe/examples/android/src/java/com/google/mediapipe/apps/aar_example:mp_face_detection_aar up-to-date: + # bazel-bin/mediapipe/examples/android/src/java/com/google/mediapipe/apps/aar_example/mp_face_detection_aar.aar + ``` + +3. (Optional) Save the AAR to your preferred location. + + ```bash + cp bazel-bin/mediapipe/examples/android/src/java/com/google/mediapipe/apps/aar_example/mp_face_detection_aar.aar + /absolute/path/to/your/preferred/location + ``` + +### Steps to use a MediaPipe AAR in Android Studio with Gradle + +1. Start Android Studio and go to your project. + +2. Copy the AAR into app/libs. + + ```bash + cp bazel-bin/mediapipe/examples/android/src/java/com/google/mediapipe/apps/aar_example/mp_face_detection_aar.aar + /path/to/your/app/libs/ + ``` + + ![Screenshot](images/mobile/aar_location.png) + +3. Make app/src/main/assets and copy assets (graph, model, and etc) into + app/src/main/assets. + + Build the MediaPipe binary graph and copy the assets into + app/src/main/assets, e.g., for the face detection graph, you need to build + and copy + [the binary graph](https://github.com/google/mediapipe/blob/master/mediapipe/examples/android/src/java/com/google/mediapipe/apps/facedetectiongpu/BUILD#L41), + [the tflite model](https://github.com/google/mediapipe/tree/master/mediapipe/models/face_detection_front.tflite), + and + [the label map](https://github.com/google/mediapipe/blob/master/mediapipe/models/face_detection_front_labelmap.txt). + + ```bash + bazel build -c opt mediapipe/examples/android/src/java/com/google/mediapipe/apps/facedetectiongpu:binary_graph + cp bazel-bin/mediapipe/examples/android/src/java/com/google/mediapipe/apps/facedetectiongpu/facedetectiongpu.binarypb /path/to/your/app/src/main/assets/ + cp mediapipe/models/face_detection_front.tflite /path/to/your/app/src/main/assets/ + cp mediapipe/models/face_detection_front_labelmap.txt /path/to/your/app/src/main/assets/ + ``` + + ![Screenshot](images/mobile/assets_location.png) + +4. Make app/src/main/jniLibs and copy OpenCV JNI libraries into + app/src/main/jniLibs. + + MediaPipe depends on OpenCV, you will need to copy the precompiled OpenCV so + files into app/src/main/jniLibs. You can download the official OpenCV + Android SDK from + [here](https://github.com/opencv/opencv/releases/download/4.1.0/opencv-4.1.0-android-sdk.zip) + and run: + + ```bash + cp -R ~/Downloads/OpenCV-android-sdk/sdk/native/libs/arm* /path/to/your/app/src/main/jniLibs/ + ``` + + ![Screenshot](images/mobile/android_studio_opencv_location.png) + +5. Modify app/build.gradle to add MediaPipe dependencies and MediaPipe AAR. + + ``` + dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + // MediaPipe deps + implementation 'com.google.flogger:flogger:0.3.1' + implementation 'com.google.flogger:flogger-system-backend:0.3.1' + implementation 'com.google.code.findbugs:jsr305:3.0.2' + implementation 'com.google.guava:guava:27.0.1-android' + implementation 'com.google.guava:guava:27.0.1-android' + // CameraX core library + def camerax_version = "1.0.0-alpha06" + implementation "androidx.camera:camera-core:$camerax_version" + implementation "androidx.camera:camera-camera2:$camerax_version" + } + ``` + +6. Follow our Android app examples to use MediaPipe in Android Studio for your + use case. If you are looking for an example, a working face detection + example can be found + [here](https://github.com/jiuqiant/mediapipe_aar_example). diff --git a/mediapipe/docs/examples.md b/mediapipe/docs/examples.md index 404024b5f..e9d27e0fc 100644 --- a/mediapipe/docs/examples.md +++ b/mediapipe/docs/examples.md @@ -96,8 +96,9 @@ using the MediaPipe C++ APIs. ### Feature Extration for YouTube-8M Challenge -[Feature Extration for YouTube-8M Challenge](./youtube_8m.md) shows how to use -MediaPipe to prepare training data for the YouTube-8M Challenge. +[Feature Extration and Model Inference for YouTube-8M Challenge](./youtube_8m.md) +shows how to use MediaPipe to prepare training data for the YouTube-8M Challenge +and do the model inference with the baseline model. ### Preparing Data Sets with MediaSequence diff --git a/mediapipe/docs/face_detection_desktop.md b/mediapipe/docs/face_detection_desktop.md index b95705262..f5e4c452c 100644 --- a/mediapipe/docs/face_detection_desktop.md +++ b/mediapipe/docs/face_detection_desktop.md @@ -36,10 +36,9 @@ $ bazel build -c opt --define MEDIAPIPE_DISABLE_GPU=1 \ # INFO: 711 processes: 710 linux-sandbox, 1 local. # INFO: Build completed successfully, 734 total actions -$ export GLOG_logtostderr=1 # This will open up your webcam as long as it is connected and on # Any errors is likely due to your webcam being not accessible -$ bazel-bin/mediapipe/examples/desktop/face_detection/face_detection_cpu \ +$ GLOG_logtostderr=1 bazel-bin/mediapipe/examples/desktop/face_detection/face_detection_cpu \ --calculator_graph_config_file=mediapipe/graphs/face_detection/face_detection_desktop_live.pbtxt ``` @@ -60,11 +59,10 @@ $ bazel build -c opt --copt -DMESA_EGL_NO_X11_HEADERS \ # INFO: 711 processes: 710 linux-sandbox, 1 local. # INFO: Build completed successfully, 734 total actions -$ export GLOG_logtostderr=1 # This will open up your webcam as long as it is connected and on # Any errors is likely due to your webcam being not accessible, # or GPU drivers not setup properly. -$ bazel-bin/mediapipe/examples/desktop/face_detection/face_detection_gpu \ +$ GLOG_logtostderr=1 bazel-bin/mediapipe/examples/desktop/face_detection/face_detection_gpu \ --calculator_graph_config_file=mediapipe/graphs/face_detection/face_detection_mobile_gpu.pbtxt ``` diff --git a/mediapipe/docs/hair_segmentation_desktop.md b/mediapipe/docs/hair_segmentation_desktop.md index 058902363..e5fd274d0 100644 --- a/mediapipe/docs/hair_segmentation_desktop.md +++ b/mediapipe/docs/hair_segmentation_desktop.md @@ -35,11 +35,10 @@ $ bazel build -c opt --copt -DMESA_EGL_NO_X11_HEADERS \ #INFO: Streaming build results to: http://sponge2/37d5a184-293b-4e98-a43e-b22084db3142 #INFO: Build completed successfully, 12210 total actions -$ export GLOG_logtostderr=1 # This will open up your webcam as long as it is connected and on # Any errors is likely due to your webcam being not accessible, # or GPU drivers not setup properly. -$ bazel-bin/mediapipe/examples/desktop/hair_segmentation/hair_segmentation_gpu \ +$ GLOG_logtostderr=1 bazel-bin/mediapipe/examples/desktop/hair_segmentation/hair_segmentation_gpu \ --calculator_graph_config_file=mediapipe/graphs/hair_segmentation/hair_segmentation_mobile_gpu.pbtxt ``` diff --git a/mediapipe/docs/hand_tracking_desktop.md b/mediapipe/docs/hand_tracking_desktop.md index 6776a4710..3e8b10c8f 100644 --- a/mediapipe/docs/hand_tracking_desktop.md +++ b/mediapipe/docs/hand_tracking_desktop.md @@ -35,10 +35,9 @@ $ bazel build -c opt --define MEDIAPIPE_DISABLE_GPU=1 \ #INFO: Streaming build results to: http://sponge2/360196b9-33ab-44b1-84a7-1022b5043307 #INFO: Build completed successfully, 12517 total actions -$ export GLOG_logtostderr=1 # This will open up your webcam as long as it is connected and on # Any errors is likely due to your webcam being not accessible -$ bazel-bin/mediapipe/examples/desktop/hand_tracking/hand_tracking_cpu \ +$ GLOG_logtostderr=1 bazel-bin/mediapipe/examples/desktop/hand_tracking/hand_tracking_cpu \ --calculator_graph_config_file=mediapipe/graphs/hand_tracking/hand_tracking_desktop_live.pbtxt ``` @@ -59,11 +58,10 @@ $ bazel build -c opt --copt -DMESA_EGL_NO_X11_HEADERS \ #INFO: Streaming build results to: http://sponge2/00c7f95f-6fbc-432d-8978-f5d361efca3b #INFO: Build completed successfully, 22455 total actions -$ export GLOG_logtostderr=1 # This will open up your webcam as long as it is connected and on # Any errors is likely due to your webcam being not accessible, # or GPU drivers not setup properly. -$ bazel-bin/mediapipe/examples/desktop/hand_tracking/hand_tracking_gpu \ +$ GLOG_logtostderr=1 bazel-bin/mediapipe/examples/desktop/hand_tracking/hand_tracking_gpu \ --calculator_graph_config_file=mediapipe/graphs/hand_tracking/hand_tracking_mobile.pbtxt ``` diff --git a/mediapipe/docs/images/mobile/aar_location.png b/mediapipe/docs/images/mobile/aar_location.png new file mode 100644 index 0000000000000000000000000000000000000000..f85e8219e89561abad3567df26c9c04b9640a773 GIT binary patch literal 35658 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 literal 0 HcmV?d00001 diff --git a/mediapipe/docs/images/mobile/android_studio_opencv_location.png b/mediapipe/docs/images/mobile/android_studio_opencv_location.png new file mode 100644 index 0000000000000000000000000000000000000000..dbb26af1ad08cba69d28a52301299a81db12dd90 GIT binary patch literal 76954 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 z Z3G?x4)Q#s-r{|_ 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 literal 0 HcmV?d00001 diff --git a/mediapipe/docs/install.md b/mediapipe/docs/install.md index 99473811e..825f5f831 100644 --- a/mediapipe/docs/install.md +++ b/mediapipe/docs/install.md @@ -24,7 +24,8 @@ Choose your operating system: To build and run Android apps: - [Setting up Android SDK and NDK](#setting-up-android-sdk-and-ndk) -- [Setting up Android Studio with MediaPipe](#setting-up-android-studio-with-mediapipe) +- [Using MediaPipe with Gradle](#using-mediapipe-with-gradle) +- [Using MediaPipe with Bazel](#using-mediapipe-with-bazel) To build and run iOS apps: @@ -41,19 +42,11 @@ To build and run iOS apps: $ cd mediapipe ``` -2. Install Bazel (0.24.1 and above required). +2. Install Bazel (version between 0.24.1 and 0.29.1). - Option 1. Use package manager tool to install the latest version of Bazel. - - ```bash - $ sudo apt-get install bazel - - # Run 'bazel version' to check version of bazel installed - ``` - - Option 2. Follow Bazel's + Follow the official [documentation](https://docs.bazel.build/versions/master/install-ubuntu.html) - to install any version of Bazel manually. + to install Bazel manually. Note that MediaPipe doesn't support Bazel 1.0.0+ yet. 3. Install OpenCV and FFmpeg. @@ -75,10 +68,10 @@ To build and run iOS apps: [documentation](https://docs.opencv.org/3.4.6/d7/d9f/tutorial_linux_install.html) to manually build OpenCV from source code. - Note: You may need to modify [`WORKSAPCE`] and [`opencv_linux.BUILD`] to + Note: You may need to modify [`WORKSPACE`] and [`opencv_linux.BUILD`] to point MediaPipe to your own OpenCV libraries, e.g., if OpenCV 4 is installed in "/usr/local/", you need to update the "linux_opencv" new_local_repository - rule in [`WORKSAPCE`] and "opencv" cc_library rule in [`opencv_linux.BUILD`] + rule in [`WORKSPACE`] and "opencv" cc_library rule in [`opencv_linux.BUILD`] like the following: ```bash @@ -159,11 +152,11 @@ To build and run iOS apps: $ cd mediapipe ``` -2. Install Bazel (0.24.1 and above required). +2. Install Bazel (version between 0.24.1 and 0.29.1). - Follow Bazel's + Follow the official [documentation](https://docs.bazel.build/versions/master/install-redhat.html) - to install Bazel manually. + to install Bazel manually. Note that MediaPipe doesn't support Bazel 1.0.0+ yet. 3. Install OpenCV. @@ -178,10 +171,10 @@ To build and run iOS apps: Option 2. Build OpenCV from source code. - Note: You may need to modify [`WORKSAPCE`] and [`opencv_linux.BUILD`] to + Note: You may need to modify [`WORKSPACE`] and [`opencv_linux.BUILD`] to point MediaPipe to your own OpenCV libraries, e.g., if OpenCV 4 is installed in "/usr/local/", you need to update the "linux_opencv" new_local_repository - rule in [`WORKSAPCE`] and "opencv" cc_library rule in [`opencv_linux.BUILD`] + rule in [`WORKSPACE`] and "opencv" cc_library rule in [`opencv_linux.BUILD`] like the following: ```bash @@ -237,7 +230,7 @@ To build and run iOS apps: * Install [Homebrew](https://brew.sh). * Install [Xcode](https://developer.apple.com/xcode/) and its Command Line - Tools. + Tools by `xcode-select install`. 2. Checkout MediaPipe repository. @@ -247,19 +240,24 @@ To build and run iOS apps: $ cd mediapipe ``` -3. Install Bazel (0.24.1 and above required). +3. Install Bazel (version between 0.24.1 and 0.29.1). - Option 1. Use package manager tool to install the latest version of Bazel. + Option 1. Use package manager tool to install Bazel 0.29.1 ```bash - $ brew install bazel + # If Bazel 1.0.0+ was installed. + $ brew uninstall bazel + + # Install Bazel 0.29.1 + $ brew install https://raw.githubusercontent.com/bazelbuild/homebrew-tap/223ffb570c21c0a2af251afc6df9dec0214c6e74/Formula/bazel.rb + $ brew link bazel # Run 'bazel version' to check version of bazel installed ``` - Option 2. Follow Bazel's + Option 2. Follow the official [documentation](https://docs.bazel.build/versions/master/install-os-x.html#install-with-installer-mac-os-x) - to install any version of Bazel manually. + to install Bazel manually. Note that MediaPipe doesn't support Bazel 1.0.0+ yet. 4. Install OpenCV and FFmpeg. @@ -281,7 +279,7 @@ To build and run iOS apps: $ port install opencv ``` - Note: when using MacPorts, please edit the [`WORKSAPCE`], + Note: when using MacPorts, please edit the [`WORKSPACE`], [`opencv_macos.BUILD`], and [`ffmpeg_macos.BUILD`] files like the following: ```bash @@ -419,10 +417,10 @@ To build and run iOS apps: [documentation](https://docs.opencv.org/3.4.6/d7/d9f/tutorial_linux_install.html) to manually build OpenCV from source code. - Note: You may need to modify [`WORKSAPCE`] and [`opencv_linux.BUILD`] to + Note: You may need to modify [`WORKSPACE`] and [`opencv_linux.BUILD`] to point MediaPipe to your own OpenCV libraries, e.g., if OpenCV 4 is installed in "/usr/local/", you need to update the "linux_opencv" new_local_repository - rule in [`WORKSAPCE`] and "opencv" cc_library rule in [`opencv_linux.BUILD`] + rule in [`WORKSPACE`] and "opencv" cc_library rule in [`opencv_linux.BUILD`] like the following: ```bash @@ -589,10 +587,20 @@ Please verify all the necessary packages are installed. * Android SDK Tools 26.1.1 * Android NDK 17c or above -### Setting up Android Studio with MediaPipe +### Using MediaPipe with Gradle -The steps below use Android Studio 3.5 to build and install a MediaPipe example -app. +MediaPipe can be used within an existing project, such as a Gradle project, +using the MediaPipe AAR target defined in mediapipe_aar.bzl. Please see the +separate [MediaPipe Android Archive Library](./android_archive_library.md) +documentation. + +### Using MediaPipe with Bazel + +The MediaPipe project can be imported to Android Studio using the Bazel plugins. +This allows the MediaPipe examples and demos to be built and modified in Android +Studio. To incorporate MediaPipe into an existing Android Studio project, see: +"Using MediaPipe with Gradle". The steps below use Android Studio 3.5 to build +and install a MediaPipe example app. 1. Install and launch Android Studio 3.5. @@ -682,7 +690,7 @@ app. * Press the `[+]` button to add the new configuration. * Select `Run` to run the example app on the connected Android device. -[`WORKSAPCE`]: https://github.com/google/mediapipe/tree/master/WORKSPACE +[`WORKSPACE`]: https://github.com/google/mediapipe/tree/master/WORKSPACE [`opencv_linux.BUILD`]: https://github.com/google/mediapipe/tree/master/third_party/opencv_linux.BUILD [`opencv_macos.BUILD`]: https://github.com/google/mediapipe/tree/master/third_party/opencv_macos.BUILD [`ffmpeg_macos.BUILD`]:https://github.com/google/mediapipe/tree/master/third_party/ffmpeg_macos.BUILD diff --git a/mediapipe/docs/object_detection_desktop.md b/mediapipe/docs/object_detection_desktop.md index 63de4f1ef..ceb9da362 100644 --- a/mediapipe/docs/object_detection_desktop.md +++ b/mediapipe/docs/object_detection_desktop.md @@ -35,10 +35,9 @@ $ bazel build -c opt \ # INFO: 2675 processes: 2673 linux-sandbox, 2 local. # INFO: Build completed successfully, 2807 total actions -$ export GLOG_logtostderr=1 # Replace and . # You can find a test video in mediapipe/examples/desktop/object_detection. -$ bazel-bin/mediapipe/examples/desktop/object_detection/object_detection_tensorflow \ +$ GLOG_logtostderr=1 bazel-bin/mediapipe/examples/desktop/object_detection/object_detection_tensorflow \ --calculator_graph_config_file=mediapipe/graphs/object_detection/object_detection_desktop_tensorflow_graph.pbtxt \ --input_side_packets=input_video_path=,output_video_path= ``` @@ -200,10 +199,9 @@ $ bazel build -c opt --define MEDIAPIPE_DISABLE_GPU=1 \ # INFO: 711 processes: 710 linux-sandbox, 1 local. # INFO: Build completed successfully, 734 total actions -$ export GLOG_logtostderr=1 # Replace and . # You can find a test video in mediapipe/examples/desktop/object_detection. -$ bazel-bin/mediapipe/examples/desktop/object_detection/object_detection_tflite \ +$ GLOG_logtostderr=1 bazel-bin/mediapipe/examples/desktop/object_detection/object_detection_tflite \ --calculator_graph_config_file=mediapipe/graphs/object_detection/object_detection_desktop_tflite_graph.pbtxt \ --input_side_packets=input_video_path=,output_video_path= ``` @@ -224,10 +222,9 @@ $ bazel build -c opt --define MEDIAPIPE_DISABLE_GPU=1 \ #INFO: Streaming build results to: http://sponge2/1824d4cc-ba63-4350-bdc0-aacbd45b902b #INFO: Build completed successfully, 12154 total actions -$ export GLOG_logtostderr=1 # This will open up your webcam as long as it is connected and on # Any errors is likely due to your webcam being not accessible -$ bazel-bin/mediapipe/examples/desktop/object_detection/object_detection_cpu \ +$ GLOG_logtostderr=1 bazel-bin/mediapipe/examples/desktop/object_detection/object_detection_cpu \ --calculator_graph_config_file=mediapipe/graphs/object_detection/object_detection_desktop_live.pbtxt ``` diff --git a/mediapipe/docs/youtube_8m.md b/mediapipe/docs/youtube_8m.md index dc6b26012..65346a6d3 100644 --- a/mediapipe/docs/youtube_8m.md +++ b/mediapipe/docs/youtube_8m.md @@ -1,9 +1,11 @@ -## Extracting Video Features for YouTube-8M Challenge +# Feature Extration and Model Inference for YouTube-8M Challenge 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 for the -YouTube-8M Challenge. +model development by demonstrating how to prepare training data and do model +inference for the YouTube-8M Challenge. + +## Extracting Video Features for YouTube-8M Challenge [Youtube-8M Challenge](https://www.kaggle.com/c/youtube8m-2019) is an annual video classification challenge hosted by Google. Over the last two years, the @@ -29,14 +31,14 @@ videos. ### Steps to run the YouTube-8M feature extraction graph -1. Checkout the mediapipe repository +1. Checkout the mediapipe repository. ```bash git clone https://github.com/google/mediapipe.git cd mediapipe ``` -2. Download the PCA and model data +2. Download the PCA and model data. ```bash mkdir /tmp/mediapipe @@ -49,7 +51,7 @@ videos. tar -xvf /tmp/mediapipe/inception-2015-12-05.tgz ``` -3. Get the VGGish frozen graph +3. Get the VGGish frozen graph. Note: To run step 3 and step 4, you must have Python 2.7 or 3.5+ installed with the TensorFlow 1.14+ package installed. @@ -60,24 +62,103 @@ videos. python -m mediapipe.examples.desktop.youtube8m.generate_vggish_frozen_graph ``` -4. Generate a MediaSequence metadata from the input video +4. Generate a MediaSequence metadata from the input video. Note: the output file is /tmp/mediapipe/metadata.tfrecord ```bash + # change clip_end_time_sec to match the length of your video. python -m mediapipe.examples.desktop.youtube8m.generate_input_sequence_example \ - --path_to_input_video=/absolute/path/to/the/local/video/file + --path_to_input_video=/absolute/path/to/the/local/video/file \ + --clip_end_time_sec=120 ``` -5. Run the MediaPipe binary to extract the features +5. Run the MediaPipe binary to extract the features. ```bash bazel build -c opt \ --define MEDIAPIPE_DISABLE_GPU=1 --define no_aws_support=true \ mediapipe/examples/desktop/youtube8m:extract_yt8m_features - ./bazel-bin/mediapipe/examples/desktop/youtube8m/extract_yt8m_features + GLOG_logtostderr=1 bazel-bin/mediapipe/examples/desktop/youtube8m/extract_yt8m_features \ --calculator_graph_config_file=mediapipe/graphs/youtube8m/feature_extraction.pbtxt \ --input_side_packets=input_sequence_example=/tmp/mediapipe/metadata.tfrecord \ --output_side_packets=output_sequence_example=/tmp/mediapipe/output.tfrecord ``` + +## Model Inference for YouTube-8M Challenge + +MediaPipe can help you do model inference for YouTube-8M Challenge with both +local videos and the YouTube-8M dataset. To visualize +[the graph for local videos](https://github.com/google/mediapipe/tree/master/mediapipe/graphs/youtube8m/local_video_model_inference.pbtxt) +and +[the graph for the YouTube-8M dataset](https://github.com/google/mediapipe/tree/master/mediapipe/graphs/youtube8m/yt8m_dataset_model_inference.pbtxt), +copy the text specification of the graph and paste it into +[MediaPipe Visualizer](https://viz.mediapipe.dev/). We use the baseline model +[(model card)](https://drive.google.com/file/d/1xTCi9-Nm9dt2KIk8WR0dDFrIssWawyXy/view) +in our example. But, the model inference pipeline is highly customizable. You +are welcome to add new calculators or use your own machine learning models to do +the inference for both local videos and the dataset + +### Steps to run the YouTube-8M model inference graph with Web Interface + +1. Copy the baseline model + [(model card)](https://drive.google.com/file/d/1xTCi9-Nm9dt2KIk8WR0dDFrIssWawyXy/view) + to local. + + ```bash + curl -o /tmp/mediapipe/yt8m_baseline_saved_model.tar.gz data.yt8m.org/models/baseline/saved_model.tar.gz + + tar -xvf /tmp/mediapipe/yt8m_baseline_saved_model.tar.gz -C /tmp/mediapipe + ``` + +2. Build the inference binary. + + ```bash + bazel build -c opt --define='MEDIAPIPE_DISABLE_GPU=1' \ + mediapipe/examples/desktop/youtube8m:model_inference + ``` + +3. Run the python web server. + + Note: pip install absl-py + + ```bash + python mediapipe/examples/desktop/youtube8m/viewer/server.py --root `pwd` + ``` + + Navigate to localhost:8008 in a web browser. + [Here](https://drive.google.com/file/d/19GSvdAAuAlACpBhHOaqMWZ_9p8bLUYKh/view?usp=sharing) + is a demo video showing the steps to use this web application. Also please + read + [youtube8m/README.md](https://github.com/google/mediapipe/tree/master/mediapipe/examples/desktop/youtube8m/README.md) + if you prefer to run the underlying model_inference binary in command line. + +### Steps to run the YouTube-8M model inference graph with a local video + +1. Make sure you have the output tfrecord from the feature extraction pipeline. + +2. Copy the baseline model + [(model card)](https://drive.google.com/file/d/1xTCi9-Nm9dt2KIk8WR0dDFrIssWawyXy/view) + to local. + + ```bash + curl -o /tmp/mediapipe/yt8m_baseline_saved_model.tar.gz data.yt8m.org/models/baseline/saved_model.tar.gz + + tar -xvf /tmp/mediapipe/yt8m_baseline_saved_model.tar.gz -C /tmp/mediapipe + ``` + +3. Build and run the inference binary. + + ```bash + bazel build -c opt --define='MEDIAPIPE_DISABLE_GPU=1' \ + mediapipe/examples/desktop/youtube8m:model_inference + + # segment_size is the number of seconds window of frames. + # overlap is the number of seconds adjacent segments share. + GLOG_logtostderr=1 bazel-bin/mediapipe/examples/desktop/youtube8m/model_inference \ + --calculator_graph_config_file=mediapipe/graphs/youtube8m/local_video_model_inference.pbtxt \ + --input_side_packets=input_sequence_example_path=/tmp/mediapipe/output.tfrecord,input_video_path=/absolute/path/to/the/local/video/file,output_video_path=/tmp/mediapipe/annotated_video.mp4,segment_size=5,overlap=4 + ``` + +4. View the annotated video. diff --git a/mediapipe/examples/desktop/BUILD b/mediapipe/examples/desktop/BUILD index 3a35d724b..f579c49e5 100644 --- a/mediapipe/examples/desktop/BUILD +++ b/mediapipe/examples/desktop/BUILD @@ -27,7 +27,9 @@ cc_library( "//mediapipe/framework/port:file_helpers", "//mediapipe/framework/port:map_util", "//mediapipe/framework/port:parse_text_proto", + "//mediapipe/framework/port:ret_check", "//mediapipe/framework/port:status", + "//mediapipe/framework/port:statusor", "@com_google_absl//absl/strings", ], ) diff --git a/mediapipe/examples/desktop/simple_run_graph_main.cc b/mediapipe/examples/desktop/simple_run_graph_main.cc index c912837f8..ee54bf231 100644 --- a/mediapipe/examples/desktop/simple_run_graph_main.cc +++ b/mediapipe/examples/desktop/simple_run_graph_main.cc @@ -13,14 +13,23 @@ // limitations under the License. // // A simple main function to run a MediaPipe graph. +#include +#include +#include +#include +#include +#include "absl/strings/str_cat.h" #include "absl/strings/str_split.h" +#include "absl/strings/string_view.h" #include "mediapipe/framework/calculator_framework.h" #include "mediapipe/framework/port/commandlineflags.h" #include "mediapipe/framework/port/file_helpers.h" #include "mediapipe/framework/port/map_util.h" #include "mediapipe/framework/port/parse_text_proto.h" +#include "mediapipe/framework/port/ret_check.h" #include "mediapipe/framework/port/status.h" +#include "mediapipe/framework/port/statusor.h" DEFINE_string( calculator_graph_config_file, "", @@ -31,14 +40,72 @@ DEFINE_string(input_side_packets, "", "for the CalculatorGraph. All values will be treated as the " "string type even if they represent doubles, floats, etc."); +// Local file output flags. +// Output stream +DEFINE_string(output_stream, "", + "The output stream to output to the local file in csv format."); +DEFINE_string(output_stream_file, "", + "The name of the local file to output all packets sent to " + "the stream specified with --output_stream. "); +DEFINE_bool(strip_timestamps, false, + "If true, only the packet contents (without timestamps) will be " + "written into the local file."); +// Output side packets +DEFINE_string(output_side_packets, "", + "A CSV of output side packets to output to local file."); +DEFINE_string(output_side_packets_file, "", + "The name of the local file to output all side packets specified " + "with --output_side_packets. "); + +::mediapipe::Status OutputStreamToLocalFile( + ::mediapipe::OutputStreamPoller& poller) { + std::ofstream file; + file.open(FLAGS_output_stream_file); + ::mediapipe::Packet packet; + while (poller.Next(&packet)) { + std::string output_data; + if (!FLAGS_strip_timestamps) { + absl::StrAppend(&output_data, packet.Timestamp().Value(), ","); + } + absl::StrAppend(&output_data, packet.Get(), "\n"); + file << output_data; + } + file.close(); + return ::mediapipe::OkStatus(); +} + +::mediapipe::Status OutputSidePacketsToLocalFile( + ::mediapipe::CalculatorGraph& graph) { + if (!FLAGS_output_side_packets.empty() && + !FLAGS_output_side_packets_file.empty()) { + std::ofstream file; + file.open(FLAGS_output_side_packets_file); + std::vector side_packet_names = + absl::StrSplit(FLAGS_output_side_packets, ','); + for (const std::string& side_packet_name : side_packet_names) { + ASSIGN_OR_RETURN(auto status_or_packet, + graph.GetOutputSidePacket(side_packet_name)); + file << absl::StrCat(side_packet_name, ":", + status_or_packet.Get(), "\n"); + } + file.close(); + } else { + RET_CHECK(FLAGS_output_side_packets.empty() && + FLAGS_output_side_packets_file.empty()) + << "--output_side_packets and --output_side_packets_file should be " + "specified in pair."; + } + return ::mediapipe::OkStatus(); +} + ::mediapipe::Status RunMPPGraph() { std::string calculator_graph_config_contents; - MP_RETURN_IF_ERROR(mediapipe::file::GetContents( + MP_RETURN_IF_ERROR(::mediapipe::file::GetContents( FLAGS_calculator_graph_config_file, &calculator_graph_config_contents)); LOG(INFO) << "Get calculator graph config contents: " << calculator_graph_config_contents; - mediapipe::CalculatorGraphConfig config = - mediapipe::ParseTextProtoOrDie( + ::mediapipe::CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie<::mediapipe::CalculatorGraphConfig>( calculator_graph_config_contents); std::map input_side_packets; std::vector kv_pairs = @@ -51,10 +118,23 @@ DEFINE_string(input_side_packets, "", ::mediapipe::MakePacket(name_and_value[1]); } LOG(INFO) << "Initialize the calculator graph."; - mediapipe::CalculatorGraph graph; + ::mediapipe::CalculatorGraph graph; MP_RETURN_IF_ERROR(graph.Initialize(config, input_side_packets)); - LOG(INFO) << "Start running the calculator graph."; - return graph.Run(); + if (!FLAGS_output_stream.empty() && !FLAGS_output_stream_file.empty()) { + ASSIGN_OR_RETURN(auto poller, + graph.AddOutputStreamPoller(FLAGS_output_stream)); + LOG(INFO) << "Start running the calculator graph."; + MP_RETURN_IF_ERROR(graph.StartRun({})); + MP_RETURN_IF_ERROR(OutputStreamToLocalFile(poller)); + } else { + RET_CHECK(FLAGS_output_stream.empty() && FLAGS_output_stream_file.empty()) + << "--output_stream and --output_stream_file should be specified in " + "pair."; + LOG(INFO) << "Start running the calculator graph."; + MP_RETURN_IF_ERROR(graph.StartRun({})); + } + MP_RETURN_IF_ERROR(graph.WaitUntilDone()); + return OutputSidePacketsToLocalFile(graph); } int main(int argc, char** argv) { diff --git a/mediapipe/examples/desktop/youtube8m/BUILD b/mediapipe/examples/desktop/youtube8m/BUILD index c25c5f50d..16b868bdc 100644 --- a/mediapipe/examples/desktop/youtube8m/BUILD +++ b/mediapipe/examples/desktop/youtube8m/BUILD @@ -33,3 +33,14 @@ cc_binary( "@org_tensorflow//tensorflow/core:direct_session", ], ) + +cc_binary( + name = "model_inference", + deps = [ + "//mediapipe/examples/desktop:simple_run_graph_main", + "//mediapipe/graphs/youtube8m:yt8m_inference_calculators_deps", + # TODO: Figure out the minimum set of the kernels needed by this example. + "@org_tensorflow//tensorflow/core:all_kernels", + "@org_tensorflow//tensorflow/core:direct_session", + ], +) diff --git a/mediapipe/examples/desktop/youtube8m/README.md b/mediapipe/examples/desktop/youtube8m/README.md index 2989a7927..8ad5bf482 100644 --- a/mediapipe/examples/desktop/youtube8m/README.md +++ b/mediapipe/examples/desktop/youtube8m/README.md @@ -1,13 +1,13 @@ ### Steps to run the YouTube-8M feature extraction graph -1. Checkout the mediapipe repository +1. Checkout the mediapipe repository. ```bash git clone https://github.com/google/mediapipe.git cd mediapipe ``` -2. Download the PCA and model data +2. Download the PCA and model data. ```bash mkdir /tmp/mediapipe @@ -20,7 +20,7 @@ tar -xvf /tmp/mediapipe/inception-2015-12-05.tgz ``` -3. Get the VGGish frozen graph +3. Get the VGGish frozen graph. Note: To run step 3 and step 4, you must have Python 2.7 or 3.5+ installed with the TensorFlow 1.14+ package installed. @@ -31,26 +31,114 @@ python -m mediapipe.examples.desktop.youtube8m.generate_vggish_frozen_graph ``` -4. Generate a MediaSequence metadata from the input video +4. Generate a MediaSequence metadata from the input video. Note: the output file is /tmp/mediapipe/metadata.tfrecord ```bash + # change clip_end_time_sec to match the length of your video. python -m mediapipe.examples.desktop.youtube8m.generate_input_sequence_example \ --path_to_input_video=/absolute/path/to/the/local/video/file \ - --clip_start_time_sec=0 \ - --clip_end_time_sec=10 + --clip_end_time_sec=120 ``` -5. Run the MediaPipe binary to extract the features +5. Run the MediaPipe binary to extract the features. ```bash bazel build -c opt \ --define MEDIAPIPE_DISABLE_GPU=1 --define no_aws_support=true \ mediapipe/examples/desktop/youtube8m:extract_yt8m_features - ./bazel-bin/mediapipe/examples/desktop/youtube8m/extract_yt8m_features \ + GLOG_logtostderr=1 bazel-bin/mediapipe/examples/desktop/youtube8m/extract_yt8m_features \ --calculator_graph_config_file=mediapipe/graphs/youtube8m/feature_extraction.pbtxt \ --input_side_packets=input_sequence_example=/tmp/mediapipe/metadata.tfrecord \ --output_side_packets=output_sequence_example=/tmp/mediapipe/output.tfrecord ``` + +### Steps to run the YouTube-8M inference graph with the YT8M dataset + +1. Download the YT8M dataset + + For example, download one shard of the training data: + + ```bash + curl http://us.data.yt8m.org/2/frame/train/trainpj.tfrecord --output /tmp/mediapipe/trainpj.tfrecord + ``` + +2. Copy the baseline model [(model card)](https://drive.google.com/file/d/1xTCi9-Nm9dt2KIk8WR0dDFrIssWawyXy/view) to local. + + ```bash + curl -o /tmp/mediapipe/yt8m_baseline_saved_model.tar.gz data.yt8m.org/models/baseline/saved_model.tar.gz + + tar -xvf /tmp/mediapipe/yt8m_baseline_saved_model.tar.gz -C /tmp/mediapipe + ``` + +3. Build and run the inference binary. + + ```bash + bazel build -c opt --define='MEDIAPIPE_DISABLE_GPU=1' \ + mediapipe/examples/desktop/youtube8m:model_inference + + GLOG_logtostderr=1 bazel-bin/mediapipe/examples/desktop/youtube8m/model_inference \ + --calculator_graph_config_file=mediapipe/graphs/youtube8m/yt8m_dataset_model_inference.pbtxt \ + --input_side_packets=tfrecord_path=/tmp/mediapipe/trainpj.tfrecord,record_index=0,desired_segment_size=5 \ + --output_stream=annotation_summary \ + --output_stream_file=/tmp/summary \ + --output_side_packets=yt8m_id \ + --output_side_packets_file=/tmp/yt8m_id + ``` + +### Steps to run the YouTube-8M model inference graph with Web Interface + +1. Copy the baseline model [(model card)](https://drive.google.com/file/d/1xTCi9-Nm9dt2KIk8WR0dDFrIssWawyXy/view) to local. + + + ```bash + curl -o /tmp/mediapipe/yt8m_baseline_saved_model.tar.gz data.yt8m.org/models/baseline/saved_model.tar.gz + + tar -xvf /tmp/mediapipe/yt8m_baseline_saved_model.tar.gz -C /tmp/mediapipe + ``` + +2. Build the inference binary. + + ```bash + bazel build -c opt --define='MEDIAPIPE_DISABLE_GPU=1' \ + mediapipe/examples/desktop/youtube8m:model_inference + ``` + +3. Run the python web server. + + Note: pip install absl-py + + ```bash + python mediapipe/examples/desktop/youtube8m/viewer/server.py --root `pwd` + ``` + + Navigate to localhost:8008 in a web browser. + +### Steps to run the YouTube-8M model inference graph with a local video + +1. Make sure you have the output tfrecord from the feature extraction pipeline. + +2. Copy the baseline model [(model card)](https://drive.google.com/file/d/1xTCi9-Nm9dt2KIk8WR0dDFrIssWawyXy/view) to local. + + ```bash + curl -o /tmp/mediapipe/yt8m_baseline_saved_model.tar.gz data.yt8m.org/models/baseline/saved_model.tar.gz + + tar -xvf /tmp/mediapipe/yt8m_baseline_saved_model.tar.gz -C /tmp/mediapipe + ``` + +3. Build and run the inference binary. + + ```bash + bazel build -c opt --define='MEDIAPIPE_DISABLE_GPU=1' \ + mediapipe/examples/desktop/youtube8m:model_inference + + # segment_size is the number of seconds window of frames. + # overlap is the number of seconds adjacent segments share. + GLOG_logtostderr=1 bazel-bin/mediapipe/examples/desktop/youtube8m/model_inference \ + --calculator_graph_config_file=mediapipe/graphs/youtube8m/local_video_model_inference.pbtxt \ + --input_side_packets=input_sequence_example_path=/tmp/mediapipe/output.tfrecord,input_video_path=/absolute/path/to/the/local/video/file,output_video_path=/tmp/mediapipe/annotated_video.mp4,segment_size=5,overlap=4 + ``` + +4. View the annotated video. diff --git a/mediapipe/examples/desktop/youtube8m/viewer/server.py b/mediapipe/examples/desktop/youtube8m/viewer/server.py new file mode 100644 index 000000000..febaad53d --- /dev/null +++ b/mediapipe/examples/desktop/youtube8m/viewer/server.py @@ -0,0 +1,262 @@ +"""Server for YouTube8M Model Inference Demo. + +Serves up both the static files for the website and provides a service that +fetches the video id and timestamp based labels for a video analyzed in a +tfrecord files. + +""" +from __future__ import print_function +import json +import os +import re +import socket +import subprocess +import sys + +from absl import app +from absl import flags +import http.client +import http.server +from six.moves.urllib import parse + +FLAGS = flags.FLAGS +flags.DEFINE_bool("show_label_at_center", False, + "Show labels at the center of the segment.") +flags.DEFINE_integer("port", 8008, "Port that the API is served over.") +flags.DEFINE_string("tmp_dir", "/tmp/mediapipe", + "Temporary asset storage location.") +flags.DEFINE_string("root", "", "MediaPipe root directory.") +# binary, pbtxt, label_map paths are relative to 'root' path +flags.DEFINE_string( + "binary", + "bazel-bin/mediapipe/examples/desktop/youtube8m/model_inference", + "Inference binary location.") +flags.DEFINE_string( + "pbtxt", + "mediapipe/graphs/youtube8m/yt8m_dataset_model_inference.pbtxt", + "Default pbtxt graph file.") +flags.DEFINE_string("label_map", "mediapipe/graphs/youtube8m/label_map.txt", + "Default label map text file.") + + +class HTTPServerV6(http.server.HTTPServer): + address_family = socket.AF_INET6 + + +class Youtube8MRequestHandler(http.server.SimpleHTTPRequestHandler): + """Static file server with /healthz support.""" + + def do_GET(self): + if self.path.startswith("/healthz"): + self.send_response(200) + self.send_header("Content-type", "text/plain") + self.send_header("Content-length", 2) + self.end_headers() + self.wfile.write("ok") + if self.path.startswith("/video"): + parsed_params = parse.urlparse(self.path) + url_params = parse.parse_qs(parsed_params.query) + + tfrecord_path = "" + segment_size = 5 + + print(url_params) + if "file" in url_params: + tfrecord_path = url_params["file"][0] + if "segments" in url_params: + segment_size = int(url_params["segments"][0]) + + self.fetch(tfrecord_path, segment_size) + + else: + if self.path == "/": + self.path = "/index.html" + # Default to serve up a local file + self.path = "/static" + self.path + http.server.SimpleHTTPRequestHandler.do_GET(self) + + def report_error(self, msg): + """Simplifies sending out a string as a 500 http response.""" + self.send_response(500) + self.send_header("Content-type", "text/plain") + self.end_headers() + if sys.version_info[0] < 3: + self.wfile.write(str(msg).encode("utf-8")) + else: + self.wfile.write(bytes(msg, "utf-8")) + + def report_missing_files(self, files): + """Sends out 500 response with missing files.""" + accumulate = "" + for file_path in files: + if not os.path.exists(file_path): + accumulate = "%s '%s'" % (accumulate, file_path) + + if accumulate: + self.report_error("Could not find:%s" % accumulate) + return True + + return False + + def fetch(self, path, segment_size): + """Returns the video id and labels for a tfrecord at a provided index.""" + + print("Received request. File=", path, "Segment Size =", segment_size) + + if (self.report_missing_files([ + "%s/%s" % (FLAGS.root, FLAGS.pbtxt), + "%s/%s" % (FLAGS.root, FLAGS.binary), + "%s/%s" % (FLAGS.root, FLAGS.label_map) + ])): + return + + # Parse the youtube video id off the end of the link or as a standalone id. + filename_match = re.match( + "(?:.*youtube.*v=)?([a-zA-Z-0-9_]{2})([a-zA-Z-0-9_]+)", path) + tfrecord_url = filename_match.expand(r"data.yt8m.org/2/j/r/\1/\1\2.js") + + print("Trying to get tfrecord via", tfrecord_url) + + connection = http.client.HTTPConnection("data.yt8m.org") + connection.request("GET", tfrecord_url) + response = connection.getresponse() + + response_object = json.loads(response.read()) + filename = response_object["filename_raw"] + index = response_object["index"] + + print("TFRecord discovered: ", filename, ", index", index) + + output_file = r"%s/%s" % (FLAGS.tmp_dir, filename) + tfrecord_url = r"http://us.data.yt8m.org/2/frame/train/%s" % filename + + connection = http.client.HTTPConnection("us.data.yt8m.org") + connection.request("HEAD", + filename_match.expand(r"/2/frame/train/%s" % filename)) + response = connection.getresponse() + if response.getheader("Content-Type") != "application/octet-stream": + self.report_error("Filename '%s' is invalid." % path) + + print(output_file, "exists on yt8m.org. Did we fetch this before?") + + if not os.path.exists(output_file): + print(output_file, "doesn't exist locally, download it now.") + return_code = subprocess.call( + ["curl", "--output", output_file, tfrecord_url], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + if return_code: + self.report_error("Could not retrieve contents from %s" % tfrecord_url) + return + else: + print(output_file, "exist locally, reuse it.") + + print("Run the graph...") + process = subprocess.Popen([ + "%s/%s" % (FLAGS.root, FLAGS.binary), + "--calculator_graph_config_file=%s/%s" % (FLAGS.root, FLAGS.pbtxt), + "--input_side_packets=tfrecord_path=%s" % output_file + + ",record_index=%d" % index + ",desired_segment_size=%d" % segment_size, + "--output_stream=annotation_summary", + "--output_stream_file=%s/labels" % FLAGS.tmp_dir, + "--output_side_packets=yt8m_id", + "--output_side_packets_file=%s/yt8m_id" % FLAGS.tmp_dir + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout_str, stderr_str = process.communicate() + process.wait() + + if stderr_str and "success" not in str(stderr_str).lower(): + self.report_error("Error executing server binary: \n%s" % stderr_str) + return + + f = open("%s/yt8m_id" % FLAGS.tmp_dir, "r") + contents = f.read() + print("yt8m_id is", contents[-5:-1]) + + curl_arg = "data.yt8m.org/2/j/i/%s/%s.js" % (contents[-5:-3], + contents[-5:-1]) + print("Grab labels from", curl_arg) + process = subprocess.Popen(["curl", curl_arg], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout = process.communicate() + process.wait() + + stdout_str = stdout[0].decode("utf-8") + + match = re.match(""".+"([^"]+)"[^"]+""", stdout_str) + final_results = { + "video_id": match.group(1), + "link": "https://www.youtube.com/watch?v=%s" % match.group(1), + "entries": [] + } + f = open("%s/labels" % FLAGS.tmp_dir, "r") + lines = f.readlines() + show_at_center = FLAGS.show_label_at_center + + print("%s/labels" % FLAGS.tmp_dir, "holds", len(lines), "entries") + for line in lines: + entry = {"labels": []} + final_results["entries"].append(entry) + first = True + for column in line.split(","): + if first: + subtract = segment_size / 2.0 if show_at_center else 0.0 + entry["time"] = float(int(column)) / 1000000.0 - subtract + first = False + else: + label_score = re.match("(.+):([0-9.]+).*", column) + if label_score: + score = float(label_score.group(2)) + entry["labels"].append({ + "label": label_score.group(1), + "score": score + }) + else: + print("empty score") + + response_json = json.dumps(final_results, indent=2, separators=(",", ": ")) + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + if sys.version_info[0] < 3: + self.wfile.write(str(response_json).encode("utf-8")) + else: + self.wfile.write(bytes(response_json, "utf-8")) + + +def update_pbtxt(): + """Update graph.pbtxt to use full path to label_map.txt.""" + edited_line = "" + lines = [] + with open("%s/%s" % (FLAGS.root, FLAGS.pbtxt), "r") as f: + lines = f.readlines() + for line in lines: + if "label_map_path" in line: + kv = line.split(":") + edited_line = kv[0] + (": \"%s/%s\"\n" % (FLAGS.root, FLAGS.label_map)) + with open("%s/%s" % (FLAGS.root, FLAGS.pbtxt), "w") as f: + for line in lines: + if "label_map_path" in line: + f.write(edited_line) + else: + f.write(line) + + +def main(unused_args): + dname = os.path.dirname(os.path.abspath(__file__)) + os.chdir(dname) + if not FLAGS.root: + print("Must specify MediaPipe root directory: --root `pwd`") + return + update_pbtxt() + port = FLAGS.port + print("Listening on port %s" % port) # pylint: disable=superfluous-parens + server = HTTPServerV6(("::", int(port)), Youtube8MRequestHandler) + server.serve_forever() + + +if __name__ == "__main__": + app.run(main) diff --git a/mediapipe/examples/desktop/youtube8m/viewer/static/index.html b/mediapipe/examples/desktop/youtube8m/viewer/static/index.html new file mode 100644 index 000000000..400aa0af0 --- /dev/null +++ b/mediapipe/examples/desktop/youtube8m/viewer/static/index.html @@ -0,0 +1,96 @@ + + + + MediaPipe: YouTube8M Model Inference Demo + + + + + + + + + + + + + + + + + diff --git a/mediapipe/examples/desktop/youtube8m/viewer/static/main.js b/mediapipe/examples/desktop/youtube8m/viewer/static/main.js new file mode 100644 index 000000000..ad66e67ea --- /dev/null +++ b/mediapipe/examples/desktop/youtube8m/viewer/static/main.js @@ -0,0 +1,217 @@ +/** + * @license + * Copyright 2019 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. + */ + +const STATE_PLAYER=0; +const STATE_COVER=1; +const STATE_SPINNER=2; + +/** +* Looks up the value of a url parameter. +* +* @param {string} param The name of the parameter. +* @return {?string} The parameter value or null if there is no such parameter. +*/ +var getUrlParameter = function(param) { + const url = decodeURIComponent(window.location.search.substring(1)); + const url_parts = url.split('&'); + for (var i = 0; i < url_parts.length; i++) { + const param_name = url_parts[i].split(/=(.*)/); + if (param_name[0] === param) { + return param_name[1] === undefined ? null : param_name[1]; + } + } +}; + +/** +* Sets the fields in the form to match the values of the URL parameters. +*/ +const updateFormFromURL = function() { + const form_elements = document.getElementById('form').elements; + const url = decodeURIComponent(window.location.search.substring(1)); + const url_parts = url.split('&'); + for (var i = 0; i < url_parts.length; i++) { + const p = url_parts[i].split(/=(.*)/); + if (p.length >= 2) { + if (form_elements[p[0]]) { + form_elements[p[0]].value = decodeURIComponent(p[1]); + } + } + } +}; + +let player = null; +let intervalID = undefined; +let entries = []; + +/** + * Constructs the embedded YouTube player. + */ +window.onYouTubeIframeAPIReady = () => { + player = new YT.Player('ytplayer', { + events: { + 'onReady': onPlayerReady, + 'onStateChange': onStateChange + } + }); +}; + + +/** + * Listens for YouTube video events. When video is playing, periodically checks + * the time signature and updates the feedback with labels. When video stops, + * shuts off interval timer to save cycles. + * @param {!Event} event YouTube API Event. + */ +function onStateChange(event) { + if (event.data === 1) { + // Youtube switched to playing. + intervalID = setInterval(function(){ + const currentTime = player.getCurrentTime(); + let winner = undefined; + let first = undefined; + for (entry of entries) { + if (!first) { + first = entry.labels; + } + if (entry.time < currentTime) { + winner = entry.labels; + } else { + break; + } + } + if (!winner) { + winner = first; + } + const threshold = + document.getElementById('form').elements['threshold'].value; + let message = ""; + for (var label of winner) { + if (label.score >= threshold) { + message = `${message}${label.label} (score: ${label.score})\n`; + } + } + $("textarea#feedback").val(message); + }); + } else { + if (intervalID) { + clearInterval(intervalID); + } + } +} + +/** + * Turns elements of the player on and off to reflect the state of the "app". + * @param {number} state One of STATE_COVER | STATE_SPINNER | STATE_PLAYER. + */ +function showState(state) { + switch(state) { + case STATE_COVER: + $('#cover').show(); + $('#spinner').hide(); + $('#ytplayer').hide(); + break; + case STATE_SPINNER: + $('#cover').hide(); + $('#spinner').show(); + $('#ytplayer').hide(); + break; + case STATE_PLAYER: + default: + $('#cover').hide(); + $('#spinner').hide(); + $('#ytplayer').show(); + break; + } +} + +/** + * Hide error field and clear its message. + */ +function hideError() { + $('#error_msg').css("visibility", "hidden").text(''); +} + +/** + * Set the error to visible and set its message. + * @param {string} msg Error message as a string. + */ +function showError(msg) { + $('#error_msg').css("visibility", "visible").text(msg); +} + +/** + * Privides numeric feedback for the slider. + */ +function connectSlider() { + $('#threshold_label').text( + `Score Threshold (${$('#threshold')[0].value})`); + $('#threshold').on('input', () => { + $('#threshold_label').text( + `Score Threshold (${$('#threshold')[0].value})`); + }); + $('#segments_label').text( + `Segment Size (${$('#segments')[0].value})`); + $('#segments').on('input', () => { + $('#segments_label').text( + `Segment Size (${$('#segments')[0].value})`); + }); +} + +/** + * Retrieve video information from backend. + * @param {string} filePath name of a tfrecord file. + * @param {number} segments desired number of segments (1-300) + */ +function fetchVideo(filePath, segments) { + const url = "/video?file=" + filePath + "&segments=" + segments; + $.ajax({ + url: url, + success: function(result) { + const videoId = result["video_id"]; + player.loadVideoById(videoId); + entries = result['entries']; + showState(STATE_PLAYER); + }, + error: (err) => { + showState(STATE_COVER); + console.log(err); + showError(err.responseText); + }, + datatype: "json" + }); +} + +/** + * Called when the embedded YouTube player has finished loading. It loads the + * requested video into the player and calls the golden6_viewer API to retrieve + * the frame-level data for that video. + */ +function onPlayerReady() { + const filePath = getUrlParameter('file') || ""; + const segments = parseInt(getUrlParameter('segments')) || 0; + + updateFormFromURL(); + hideError(); + connectSlider(); + + if (!filePath) { + return; + } + + showState(STATE_SPINNER); + fetchVideo(filePath, segments); +} diff --git a/mediapipe/framework/BUILD b/mediapipe/framework/BUILD index 90a4f672c..1a273670e 100644 --- a/mediapipe/framework/BUILD +++ b/mediapipe/framework/BUILD @@ -688,6 +688,12 @@ cc_library( cc_library( name = "demangle", hdrs = ["demangle.h"], + defines = select({ + "//mediapipe/framework/profiler:android_release": [ + "MEDIAPIPE_HAS_CXA_DEMANGLE=0", + ], + "//conditions:default": [], + }), visibility = ["//visibility:public"], ) @@ -1713,3 +1719,10 @@ cc_test( "//mediapipe/framework/tool/testdata:dub_quad_test_subgraph", ], ) + +# Expose the proto source files for building mediapipe AAR. +filegroup( + name = "protos_src", + srcs = glob(["*.proto"]), + visibility = ["//mediapipe:__subpackages__"], +) diff --git a/mediapipe/framework/calculator_graph_bounds_test.cc b/mediapipe/framework/calculator_graph_bounds_test.cc index 1b8c3e9f2..b6144c0ae 100644 --- a/mediapipe/framework/calculator_graph_bounds_test.cc +++ b/mediapipe/framework/calculator_graph_bounds_test.cc @@ -756,7 +756,7 @@ TEST(CalculatorGraphBoundsTest, BoundWithoutInputPackets) { MP_ASSERT_OK(graph.WaitUntilDone()); } -// Shows that when fixed-size-input-stream-hanlder drops packets, +// Shows that when fixed-size-input-stream-handler drops packets, // no timetamp bounds are announced. TEST(CalculatorGraphBoundsTest, FixedSizeHandlerBounds) { // LambdaCalculator with FixedSizeInputStreamHandler will drop packets @@ -876,5 +876,93 @@ TEST(CalculatorGraphBoundsTest, FixedSizeHandlerBounds) { MP_ASSERT_OK(graph.WaitUntilDone()); } +// A Calculator that outputs only the last packet from its input stream. +class LastPacketCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->Inputs().Index(0).SetAny(); + cc->Outputs().Index(0).SetAny(); + return ::mediapipe::OkStatus(); + } + ::mediapipe::Status Open(CalculatorContext* cc) final { + return ::mediapipe::OkStatus(); + } + ::mediapipe::Status Process(CalculatorContext* cc) final { + cc->Outputs().Index(0).SetNextTimestampBound(cc->InputTimestamp()); + last_packet_ = cc->Inputs().Index(0).Value(); + return ::mediapipe::OkStatus(); + } + ::mediapipe::Status Close(CalculatorContext* cc) final { + cc->Outputs().Index(0).AddPacket(last_packet_); + return ::mediapipe::OkStatus(); + } + + private: + Packet last_packet_; +}; +REGISTER_CALCULATOR(LastPacketCalculator); + +// Shows that the last packet in an input stream can be detected. +TEST(CalculatorGraphBoundsTest, LastPacketCheck) { + // LastPacketCalculator emits only the last input stream packet. + // It emits a timestamp bound after the arrival of a successor input stream + // packet or input stream close. The output "last_output" shows the + // last packet, and "output" shows the timestamp bounds. + CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie(R"( + input_stream: 'input' + output_stream: 'output' + output_stream: 'last_output' + node { + calculator: 'PassThroughCalculator' + input_stream: 'input' + output_stream: 'input_2' + } + node { + calculator: 'LastPacketCalculator' + input_stream: 'input_2' + output_stream: 'last_packet' + } + node { + calculator: 'PassThroughCalculator' + input_stream: 'input' + input_stream: 'last_packet' + output_stream: 'output' + output_stream: 'last_output' + } + )"); + CalculatorGraph graph; + std::vector output_packets; + MP_ASSERT_OK(graph.Initialize(config)); + MP_ASSERT_OK(graph.ObserveOutputStream("output", [&](const Packet& p) { + output_packets.push_back(p); + return ::mediapipe::OkStatus(); + })); + std::vector last_output_packets; + MP_ASSERT_OK(graph.ObserveOutputStream("last_output", [&](const Packet& p) { + last_output_packets.push_back(p); + return ::mediapipe::OkStatus(); + })); + MP_ASSERT_OK(graph.StartRun({})); + MP_ASSERT_OK(graph.WaitUntilIdle()); + + // Add four packets into the graph. + constexpr int kNumInputs = 4; + for (int i = 0; i < kNumInputs; ++i) { + Packet p = MakePacket(33).At(Timestamp(i)); + MP_ASSERT_OK(graph.AddPacketToInputStream("input", p)); + MP_ASSERT_OK(graph.WaitUntilIdle()); + EXPECT_EQ(i, output_packets.size()); + EXPECT_EQ(0, last_output_packets.size()); + } + + // Shutdown the graph. + MP_ASSERT_OK(graph.CloseAllPacketSources()); + MP_ASSERT_OK(graph.WaitUntilIdle()); + EXPECT_EQ(kNumInputs, output_packets.size()); + EXPECT_EQ(1, last_output_packets.size()); + MP_ASSERT_OK(graph.WaitUntilDone()); +} + } // namespace } // namespace mediapipe diff --git a/mediapipe/framework/calculator_graph_side_packet_test.cc b/mediapipe/framework/calculator_graph_side_packet_test.cc index 166826ff1..01171a6c1 100644 --- a/mediapipe/framework/calculator_graph_side_packet_test.cc +++ b/mediapipe/framework/calculator_graph_side_packet_test.cc @@ -743,5 +743,66 @@ TEST(CalculatorGraph, GetOutputSidePacket) { } } +typedef std::string HugeModel; + +// Generates an output-side-packet once for each calculator-graph. +class OutputSidePacketCachedCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->OutputSidePackets().Index(0).Set(); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Open(CalculatorContext* cc) final { + cc->OutputSidePackets().Index(0).Set(MakePacket( + R"(An expensive side-packet created only once per graph)")); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) final { + LOG(FATAL) << "Not reached."; + return ::mediapipe::OkStatus(); + } +}; +REGISTER_CALCULATOR(OutputSidePacketCachedCalculator); + +// Returns true if two packets hold the same data. +bool Equals(Packet p1, Packet p2) { + return packet_internal::GetHolder(p1) == packet_internal::GetHolder(p2); +} + +TEST(CalculatorGraph, OutputSidePacketCached) { + CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie(R"( + node { + calculator: "OutputSidePacketCachedCalculator" + output_side_packet: "model" + } + node { + calculator: "SidePacketToStreamPacketCalculator" + input_side_packet: "model" + output_stream: "output" + } + )"); + CalculatorGraph graph; + MP_ASSERT_OK(graph.Initialize(config)); + std::vector output_packets; + MP_ASSERT_OK(graph.ObserveOutputStream( + "output", [&output_packets](const Packet& packet) { + output_packets.push_back(packet); + return ::mediapipe::OkStatus(); + })); + + // Run the graph three times. + for (int run = 0; run < 3; ++run) { + MP_ASSERT_OK(graph.StartRun({})); + MP_ASSERT_OK(graph.WaitUntilDone()); + } + ASSERT_EQ(3, output_packets.size()); + for (int run = 0; run < output_packets.size(); ++run) { + EXPECT_TRUE(Equals(output_packets[0], output_packets[run])); + } +} + } // namespace } // namespace mediapipe diff --git a/mediapipe/framework/calculator_node.cc b/mediapipe/framework/calculator_node.cc index f3cd90eea..d4a81ff9d 100644 --- a/mediapipe/framework/calculator_node.cc +++ b/mediapipe/framework/calculator_node.cc @@ -391,6 +391,38 @@ void CalculatorNode::SetMaxInputStreamQueueSize(int max_queue_size) { return ::mediapipe::OkStatus(); } +namespace { +// Returns the Packet sent to an OutputSidePacket, or an empty packet +// if none available. +const Packet GetPacket(const OutputSidePacket& out) { + auto impl = dynamic_cast(&out); + return (impl == nullptr) ? Packet() : impl->GetPacket(); +} + +// Resends the output-side-packets from the previous graph run. +::mediapipe::Status ResendSidePackets(CalculatorContext* cc) { + auto& outs = cc->OutputSidePackets(); + for (CollectionItemId id = outs.BeginId(); id < outs.EndId(); ++id) { + Packet packet = GetPacket(outs.Get(id)); + if (!packet.IsEmpty()) { + // OutputSidePacket::Set re-announces the side-packet to its mirrors. + outs.Get(id).Set(packet); + } + } + return ::mediapipe::OkStatus(); +} +} // namespace + +bool CalculatorNode::OutputsAreConstant(CalculatorContext* cc) { + if (cc->Inputs().NumEntries() > 0 || cc->Outputs().NumEntries() > 0) { + return false; + } + if (input_side_packet_handler_.InputSidePacketsChanged()) { + return false; + } + return true; +} + ::mediapipe::Status CalculatorNode::OpenNode() { VLOG(2) << "CalculatorNode::OpenNode() for " << DebugName(); @@ -407,8 +439,9 @@ void CalculatorNode::SetMaxInputStreamQueueSize(int max_queue_size) { default_context, Timestamp::Unstarted()); ::mediapipe::Status result; - - { + if (OutputsAreConstant(default_context)) { + result = ResendSidePackets(default_context); + } else { MEDIAPIPE_PROFILING(OPEN, default_context); LegacyCalculatorSupport::Scoped s(default_context); result = calculator_->Open(default_context); @@ -494,7 +527,10 @@ void CalculatorNode::CloseOutputStreams(OutputStreamShardSet* outputs) { ::mediapipe::Status result; - { + if (OutputsAreConstant(default_context)) { + // Do nothing. + result = ::mediapipe::OkStatus(); + } else { MEDIAPIPE_PROFILING(CLOSE, default_context); LegacyCalculatorSupport::Scoped s(default_context); result = calculator_->Close(default_context); @@ -770,7 +806,10 @@ std::string CalculatorNode::DebugName() const { VLOG(2) << "Calling Calculator::Process() for node: " << DebugName(); - { + if (OutputsAreConstant(calculator_context)) { + // Do nothing. + result = ::mediapipe::OkStatus(); + } else { MEDIAPIPE_PROFILING(PROCESS, calculator_context); LegacyCalculatorSupport::Scoped s( calculator_context); diff --git a/mediapipe/framework/calculator_node.h b/mediapipe/framework/calculator_node.h index fd17d4ada..f39636e5d 100644 --- a/mediapipe/framework/calculator_node.h +++ b/mediapipe/framework/calculator_node.h @@ -280,6 +280,9 @@ class CalculatorNode { // Get a std::string describing the input streams. std::string DebugInputStreamNames() const; + // Returns true if all outputs will be identical to the previous graph run. + bool OutputsAreConstant(CalculatorContext* cc); + // The calculator. std::unique_ptr calculator_; // Keeps data which a Calculator subclass needs access to. diff --git a/mediapipe/framework/collection.h b/mediapipe/framework/collection.h index b3f972b0a..448968be2 100644 --- a/mediapipe/framework/collection.h +++ b/mediapipe/framework/collection.h @@ -240,6 +240,22 @@ class Collection { return tag_map_->EndId(tag); } + // Equal Collections contain equal mappings and equal elements. + bool operator==(const Collection& other) const { + if (tag_map_->Mapping() != other.TagMap()->Mapping()) { + return false; + } + for (CollectionItemId id = BeginId(); id < EndId(); ++id) { + if (Get(id) != other.Get(id)) { + return false; + } + } + return true; + } + bool operator!=(const Collection& other) const { + return !(*this == other); + } + private: // An iterator which is identical to ItType** except that the // dereference operator (operator*) does a double dereference and diff --git a/mediapipe/framework/demangle.h b/mediapipe/framework/demangle.h index e9624c5ac..45ebd1691 100644 --- a/mediapipe/framework/demangle.h +++ b/mediapipe/framework/demangle.h @@ -15,23 +15,25 @@ #ifndef MEDIAPIPE_FRAMEWORK_DEMANGLE_H_ #define MEDIAPIPE_FRAMEWORK_DEMANGLE_H_ +#ifndef MEDIAPIPE_HAS_CXA_DEMANGLE // We only support some compilers that support __cxa_demangle. // TODO: Checks if Android NDK has fixed this issue or not. #if defined(__ANDROID__) && (defined(__i386__) || defined(__x86_64__)) -#define HAS_CXA_DEMANGLE 0 +#define MEDIAPIPE_HAS_CXA_DEMANGLE 0 #elif (__GNUC__ >= 4 || (__GNUC__ >= 3 && __GNUC_MINOR__ >= 4)) && \ !defined(__mips__) -#define HAS_CXA_DEMANGLE 1 +#define MEDIAPIPE_HAS_CXA_DEMANGLE 1 #elif defined(__clang__) && !defined(_MSC_VER) -#define HAS_CXA_DEMANGLE 1 +#define MEDIAPIPE_HAS_CXA_DEMANGLE 1 #else -#define HAS_CXA_DEMANGLE 0 +#define MEDIAPIPE_HAS_CXA_DEMANGLE 0 +#endif #endif #include #include -#if HAS_CXA_DEMANGLE +#if MEDIAPIPE_HAS_CXA_DEMANGLE #include #endif @@ -65,7 +67,7 @@ namespace mediapipe { inline std::string Demangle(const char* mangled) { int status = 0; char* demangled = nullptr; -#if HAS_CXA_DEMANGLE +#if MEDIAPIPE_HAS_CXA_DEMANGLE demangled = abi::__cxa_demangle(mangled, nullptr, nullptr, &status); #endif std::string out; diff --git a/mediapipe/framework/deps/BUILD b/mediapipe/framework/deps/BUILD index f3ca5dc1d..cc84a99e7 100644 --- a/mediapipe/framework/deps/BUILD +++ b/mediapipe/framework/deps/BUILD @@ -15,10 +15,9 @@ # Description: # The dependencies of mediapipe. -licenses(["notice"]) # Apache 2.0 - load("//mediapipe/framework/port:build_config.bzl", "mediapipe_cc_proto_library") -load("//mediapipe/framework/port:build_config.bzl", "mediapipe_py_proto_library") + +licenses(["notice"]) # Apache 2.0 package(default_visibility = ["//visibility:private"]) diff --git a/mediapipe/framework/formats/image_format.proto b/mediapipe/framework/formats/image_format.proto index a367f4b62..ea99dfee4 100644 --- a/mediapipe/framework/formats/image_format.proto +++ b/mediapipe/framework/formats/image_format.proto @@ -66,5 +66,9 @@ message ImageFormat { // LAB, interleaved: one byte for L, then one byte for a, then one // byte for b for each pixel. LAB8 = 10; + + // sBGRA, interleaved: one byte for B, one byte for G, one byte for R, + // one byte for alpha or unused. This is the N32 format for Skia. + SBGRA = 11; } } diff --git a/mediapipe/framework/formats/image_frame.cc b/mediapipe/framework/formats/image_frame.cc index 996702ae2..338dfe165 100644 --- a/mediapipe/framework/formats/image_frame.cc +++ b/mediapipe/framework/formats/image_frame.cc @@ -279,6 +279,8 @@ int ImageFrame::NumberOfChannelsForFormat(ImageFormat::Format format) { return 1; case ImageFormat::LAB8: return 3; + case ImageFormat::SBGRA: + return 4; default: LOG(FATAL) << InvalidFormatString(format); } @@ -304,6 +306,8 @@ int ImageFrame::ChannelSizeForFormat(ImageFormat::Format format) { return sizeof(float); case ImageFormat::LAB8: return sizeof(uint8); + case ImageFormat::SBGRA: + return sizeof(uint8); default: LOG(FATAL) << InvalidFormatString(format); } @@ -329,6 +333,8 @@ int ImageFrame::ByteDepthForFormat(ImageFormat::Format format) { return 4; case ImageFormat::LAB8: return 1; + case ImageFormat::SBGRA: + return 1; default: LOG(FATAL) << InvalidFormatString(format); } diff --git a/mediapipe/framework/formats/image_frame_opencv.cc b/mediapipe/framework/formats/image_frame_opencv.cc index bf723cda3..bf8b908b3 100644 --- a/mediapipe/framework/formats/image_frame_opencv.cc +++ b/mediapipe/framework/formats/image_frame_opencv.cc @@ -59,6 +59,9 @@ int GetMatType(const mediapipe::ImageFormat::Format format) { case mediapipe::ImageFormat::LAB8: type = CV_8U; break; + case mediapipe::ImageFormat::SBGRA: + type = CV_8U; + break; default: // Invalid or unknown; Default to uchar. type = CV_8U; diff --git a/mediapipe/framework/formats/landmark.proto b/mediapipe/framework/formats/landmark.proto index cdc2ee151..220b3725d 100644 --- a/mediapipe/framework/formats/landmark.proto +++ b/mediapipe/framework/formats/landmark.proto @@ -32,3 +32,8 @@ message NormalizedLandmark { optional float y = 2; optional float z = 3; } + +// Group of NormalizedLandmark protos. +message NormalizedLandmarkList { + repeated NormalizedLandmark landmark = 1; +} diff --git a/mediapipe/framework/input_side_packet_handler.cc b/mediapipe/framework/input_side_packet_handler.cc index fb66f0694..ce43508d2 100644 --- a/mediapipe/framework/input_side_packet_handler.cc +++ b/mediapipe/framework/input_side_packet_handler.cc @@ -27,6 +27,7 @@ namespace mediapipe { std::function input_side_packets_ready_callback, std::function error_callback) { int missing_input_side_packet_count; + prev_input_side_packets_ = std::move(input_side_packets_); ASSIGN_OR_RETURN( input_side_packets_, tool::FillPacketSet(*input_side_packet_types, all_side_packets, @@ -41,6 +42,12 @@ namespace mediapipe { return ::mediapipe::OkStatus(); } +bool InputSidePacketHandler::InputSidePacketsChanged() { + return prev_input_side_packets_ == nullptr || + input_side_packets_ == nullptr || + *input_side_packets_ != *prev_input_side_packets_; +} + void InputSidePacketHandler::Set(CollectionItemId id, const Packet& packet) { ::mediapipe::Status status = SetInternal(id, packet); if (!status.ok()) { diff --git a/mediapipe/framework/input_side_packet_handler.h b/mediapipe/framework/input_side_packet_handler.h index 5112731da..ecfa2239e 100644 --- a/mediapipe/framework/input_side_packet_handler.h +++ b/mediapipe/framework/input_side_packet_handler.h @@ -52,6 +52,10 @@ class InputSidePacketHandler { const PacketSet& InputSidePackets() const { return *input_side_packets_; } + // Returns true if the set of input-side-packets has changed since the + // previous run. + bool InputSidePacketsChanged(); + // Returns the number of missing input side packets. int MissingInputSidePacketCount() const { return missing_input_side_packet_count_.load(std::memory_order_relaxed); @@ -68,6 +72,7 @@ class InputSidePacketHandler { const PacketTypeSet* input_side_packet_types_; std::unique_ptr input_side_packets_; + std::unique_ptr prev_input_side_packets_; std::atomic missing_input_side_packet_count_{0}; diff --git a/mediapipe/framework/output_side_packet_impl.cc b/mediapipe/framework/output_side_packet_impl.cc index 09cc294ff..f2771da5d 100644 --- a/mediapipe/framework/output_side_packet_impl.cc +++ b/mediapipe/framework/output_side_packet_impl.cc @@ -30,7 +30,7 @@ namespace mediapipe { void OutputSidePacketImpl::PrepareForRun( std::function error_callback) { error_callback_ = std::move(error_callback); - packet_ = Packet(); + initialized_ = false; } void OutputSidePacketImpl::Set(const Packet& packet) { @@ -47,7 +47,7 @@ void OutputSidePacketImpl::AddMirror( } ::mediapipe::Status OutputSidePacketImpl::SetInternal(const Packet& packet) { - if (!packet_.IsEmpty()) { + if (initialized_) { return ::mediapipe::AlreadyExistsErrorBuilder(MEDIAPIPE_LOC) << "Output side packet \"" << name_ << "\" was already set."; } @@ -72,6 +72,7 @@ void OutputSidePacketImpl::AddMirror( } packet_ = packet; + initialized_ = true; for (const auto& mirror : mirrors_) { mirror.input_side_packet_handler->Set(mirror.id, packet_); } diff --git a/mediapipe/framework/output_side_packet_impl.h b/mediapipe/framework/output_side_packet_impl.h index c654769c5..df9ac4082 100644 --- a/mediapipe/framework/output_side_packet_impl.h +++ b/mediapipe/framework/output_side_packet_impl.h @@ -80,6 +80,7 @@ class OutputSidePacketImpl : public OutputSidePacket { const PacketType* packet_type_; std::function error_callback_; Packet packet_; + bool initialized_ = false; std::vector mirrors_; }; diff --git a/mediapipe/framework/packet.h b/mediapipe/framework/packet.h index 8782d924c..8564abce6 100644 --- a/mediapipe/framework/packet.h +++ b/mediapipe/framework/packet.h @@ -653,6 +653,14 @@ Packet PointToForeign(const T* ptr) { return packet_internal::Create(new packet_internal::ForeignHolder(ptr)); } +// Equal Packets refer to the same memory contents, like equal pointers. +inline bool operator==(const Packet& p1, const Packet& p2) { + return packet_internal::GetHolder(p1) == packet_internal::GetHolder(p2); +} +inline bool operator!=(const Packet& p1, const Packet& p2) { + return !(p1 == p2); +} + } // namespace mediapipe #endif // MEDIAPIPE_FRAMEWORK_PACKET_H_ diff --git a/mediapipe/framework/port.h b/mediapipe/framework/port.h index c45a4546d..275f8ca98 100644 --- a/mediapipe/framework/port.h +++ b/mediapipe/framework/port.h @@ -28,4 +28,22 @@ #define MEDIAPIPE_MOBILE #endif +#if !defined(MEDIAPIPE_ANDROID) && defined(__ANDROID__) +#define MEDIAPIPE_ANDROID +#endif + +#if defined(__APPLE__) +#include "TargetConditionals.h" // for TARGET_OS_* +#if !defined(MEDIAPIPE_IOS) && !TARGET_OS_OSX +#define MEDIAPIPE_IOS +#endif +#endif + +// These platforms do not support OpenGL ES Compute Shaders (v3.1 and up), +// but can still run OpenGL ES 3.0 and below. +#if !defined(MEDIAPIPE_DISABLE_GL_COMPUTE) && \ + (defined(__APPLE__) || defined(__EMSCRIPTEN__)) +#define MEDIAPIPE_DISABLE_GL_COMPUTE +#endif + #endif // MEDIAPIPE_FRAMEWORK_PORT_H_ diff --git a/mediapipe/framework/profiler/graph_profiler_test.cc b/mediapipe/framework/profiler/graph_profiler_test.cc index cf7717556..86c6a16c3 100644 --- a/mediapipe/framework/profiler/graph_profiler_test.cc +++ b/mediapipe/framework/profiler/graph_profiler_test.cc @@ -247,25 +247,45 @@ TEST_F(GraphProfilerTestPeer, InitializeConfig) { // Checks histogram_interval_size_usec and num_histogram_intervals. CalculatorProfile actual = GetCalculatorProfilesMap()->find(kDummyTestCalculatorName)->second; - ASSERT_EQ(actual.name(), kDummyTestCalculatorName); - ASSERT_FALSE(actual.has_open_runtime()); - ASSERT_FALSE(actual.has_close_runtime()); - - ASSERT_EQ(actual.process_runtime().interval_size_usec(), 1000); - ASSERT_EQ(actual.process_runtime().num_intervals(), 3); - - ASSERT_EQ(actual.process_input_latency().interval_size_usec(), 1000); - ASSERT_EQ(actual.process_input_latency().num_intervals(), 3); - - ASSERT_EQ(actual.process_output_latency().interval_size_usec(), 1000); - ASSERT_EQ(actual.process_output_latency().num_intervals(), 3); - - ASSERT_EQ(actual.input_stream_profiles().size(), 1); - ASSERT_EQ(actual.input_stream_profiles(0).name(), "input_stream"); - ASSERT_FALSE(actual.input_stream_profiles(0).back_edge()); - ASSERT_EQ(actual.input_stream_profiles(0).latency().interval_size_usec(), - 1000); - ASSERT_EQ(actual.input_stream_profiles(0).latency().num_intervals(), 3); + EXPECT_THAT(actual, EqualsProto(R"( + name: "DummyTestCalculator" + process_runtime { + total: 0 + interval_size_usec: 1000 + num_intervals: 3 + count: 0 + count: 0 + count: 0 + } + process_input_latency { + total: 0 + interval_size_usec: 1000 + num_intervals: 3 + count: 0 + count: 0 + count: 0 + } + process_output_latency { + total: 0 + interval_size_usec: 1000 + num_intervals: 3 + count: 0 + count: 0 + count: 0 + } + input_stream_profiles { + name: "input_stream" + back_edge: false + latency { + total: 0 + interval_size_usec: 1000 + num_intervals: 3 + count: 0 + count: 0 + count: 0 + } + } + )")); } // Tests that Initialize() uses the ProfilerConfig in the graph definition. @@ -291,16 +311,17 @@ TEST_F(GraphProfilerTestPeer, InitializeConfigWithoutStreamLatency) { // Checks histogram_interval_size_usec and num_histogram_intervals. CalculatorProfile actual = GetCalculatorProfilesMap()->find(kDummyTestCalculatorName)->second; - ASSERT_EQ(actual.name(), kDummyTestCalculatorName); - ASSERT_FALSE(actual.has_open_runtime()); - ASSERT_FALSE(actual.has_close_runtime()); - - ASSERT_EQ(actual.process_runtime().interval_size_usec(), 1000); - ASSERT_EQ(actual.process_runtime().num_intervals(), 3); - - ASSERT_FALSE(actual.has_process_input_latency()); - ASSERT_FALSE(actual.has_process_output_latency()); - ASSERT_EQ(actual.input_stream_profiles().size(), 0); + EXPECT_THAT(actual, EqualsProto(R"( + name: "DummyTestCalculator" + process_runtime { + total: 0 + interval_size_usec: 1000 + num_intervals: 3 + count: 0 + count: 0 + count: 0 + } + )")); } // Tests that Initialize() reads all the configs defined in the graph @@ -633,10 +654,11 @@ TEST_F(GraphProfilerTestPeer, SetOpenRuntime) { simulation_clock->ThreadFinish(); ASSERT_EQ(profiles.size(), 1); - ASSERT_EQ(profiles[0].open_runtime(), 100); - ASSERT_FALSE(profiles[0].has_close_runtime()); - ASSERT_THAT(profiles[0].process_runtime(), - Partially(EqualsProto(CreateTimeHistogram(/*total=*/0, {0})))); + EXPECT_THAT(profiles[0], Partially(EqualsProto(R"( + name: "DummyTestCalculator" + open_runtime: 100 + process_runtime { total: 0 } + )"))); // Checks packets_info_ map hasn't changed. ASSERT_EQ(GetPacketsInfoMap()->size(), 0); } @@ -688,14 +710,29 @@ TEST_F(GraphProfilerTestPeer, SetOpenRuntimeWithStreamLatency) { ASSERT_EQ(profiles.size(), 2); CalculatorProfile source_profile = GetProfileWithName(profiles, "source_calc"); - ASSERT_EQ(source_profile.open_runtime(), 150); - ASSERT_FALSE(source_profile.has_close_runtime()); - ASSERT_THAT(source_profile.process_runtime(), - Partially(EqualsProto(CreateTimeHistogram(/*total=*/0, {0})))); - ASSERT_THAT(source_profile.process_input_latency(), - Partially(EqualsProto(CreateTimeHistogram(/*total=*/0, {0})))); - ASSERT_THAT(source_profile.process_output_latency(), - Partially(EqualsProto(CreateTimeHistogram(/*total=*/0, {0})))); + + EXPECT_THAT(source_profile, EqualsProto(R"( + name: "source_calc" + open_runtime: 150 + process_runtime { + total: 0 + interval_size_usec: 1000000 + num_intervals: 1 + count: 0 + } + process_input_latency { + total: 0 + interval_size_usec: 1000000 + num_intervals: 1 + count: 0 + } + process_output_latency { + total: 0 + interval_size_usec: 1000000 + num_intervals: 1 + count: 0 + } + )")); // Check packets_info_ map has been updated. ASSERT_EQ(GetPacketsInfoMap()->size(), 1); @@ -736,11 +773,16 @@ TEST_F(GraphProfilerTestPeer, SetCloseRuntime) { std::vector profiles = Profiles(); simulation_clock->ThreadFinish(); - ASSERT_EQ(profiles.size(), 1); - ASSERT_FALSE(profiles[0].open_runtime()); - ASSERT_EQ(profiles[0].close_runtime(), 100); - ASSERT_THAT(profiles[0].process_runtime(), - Partially(EqualsProto(CreateTimeHistogram(/*total=*/0, {0})))); + EXPECT_THAT(profiles[0], EqualsProto(R"( + name: "DummyTestCalculator" + close_runtime: 100 + process_runtime { + total: 0 + interval_size_usec: 1000000 + num_intervals: 1 + count: 0 + } + )")); } // Tests that SetCloseRuntime() updates |close_runtime| and doesn't affect other @@ -789,11 +831,39 @@ TEST_F(GraphProfilerTestPeer, SetCloseRuntimeWithStreamLatency) { ASSERT_EQ(profiles.size(), 2); CalculatorProfile source_profile = GetProfileWithName(profiles, "source_calc"); - ASSERT_FALSE(source_profile.open_runtime()); - ASSERT_EQ(source_profile.close_runtime(), 100); - ASSERT_THAT(source_profile.process_runtime(), - Partially(EqualsProto(CreateTimeHistogram(/*total=*/0, {0})))); - ASSERT_EQ(GetPacketsInfoMap()->size(), 1); + + EXPECT_THAT(source_profile, EqualsProto(R"( + name: "source_calc" + close_runtime: 100 + process_runtime { + total: 0 + interval_size_usec: 1000000 + num_intervals: 1 + count: 0 + } + process_input_latency { + total: 0 + interval_size_usec: 1000000 + num_intervals: 1 + count: 0 + } + process_output_latency { + total: 0 + interval_size_usec: 1000000 + num_intervals: 1 + count: 0 + } + input_stream_profiles { + name: "input_stream" + back_edge: false + latency { + total: 0 + interval_size_usec: 1000000 + num_intervals: 1 + count: 0 + } + } + )")); PacketInfo expected_packet_info = {0, /*production_time_usec=*/1000 + 100, /*source_process_start_usec=*/1000 + 0}; @@ -933,10 +1003,15 @@ TEST_F(GraphProfilerTestPeer, AddProcessSample) { simulation_clock->ThreadFinish(); ASSERT_EQ(profiles.size(), 1); - ASSERT_THAT(profiles[0].process_runtime(), - Partially(EqualsProto(CreateTimeHistogram(/*total=*/150, {1})))); - ASSERT_FALSE(profiles[0].has_open_runtime()); - ASSERT_FALSE(profiles[0].has_close_runtime()); + EXPECT_THAT(profiles[0], EqualsProto(R"( + name: "DummyTestCalculator" + process_runtime { + total: 150 + interval_size_usec: 1000000 + num_intervals: 1 + count: 1 + } + )")); // Checks packets_info_ map hasn't changed. ASSERT_EQ(GetPacketsInfoMap()->size(), 0); } @@ -985,12 +1060,27 @@ TEST_F(GraphProfilerTestPeer, AddProcessSampleWithStreamLatency) { ASSERT_EQ(profiles.size(), 2); CalculatorProfile source_profile = GetProfileWithName(profiles, "source_calc"); - ASSERT_THAT(source_profile.process_runtime(), - Partially(EqualsProto(CreateTimeHistogram(/*total=*/150, {1})))); - ASSERT_THAT(source_profile.process_input_latency(), - Partially(EqualsProto(CreateTimeHistogram(/*total=*/0, {1})))); - ASSERT_THAT(source_profile.process_output_latency(), - Partially(EqualsProto(CreateTimeHistogram(/*total=*/150, {1})))); + + EXPECT_THAT(profiles[0], Partially(EqualsProto(R"( + process_runtime { + total: 150 + interval_size_usec: 1000000 + num_intervals: 1 + count: 1 + } + process_input_latency { + total: 0 + interval_size_usec: 1000000 + num_intervals: 1 + count: 1 + } + process_output_latency { + total: 150 + interval_size_usec: 1000000 + num_intervals: 1 + count: 1 + } + )"))); // Check packets_info_ map has been updated. ASSERT_EQ(GetPacketsInfoMap()->size(), 1); @@ -1019,22 +1109,24 @@ TEST_F(GraphProfilerTestPeer, AddProcessSampleWithStreamLatency) { CalculatorProfile consumer_profile = GetProfileWithName(profiles, "consumer_calc"); - ASSERT_THAT(consumer_profile.process_runtime(), - Partially(EqualsProto(CreateTimeHistogram(/*total=*/250, {1})))); - ASSERT_THAT(consumer_profile.process_input_latency(), - Partially(EqualsProto(CreateTimeHistogram( - /*total=*/2000 - when_source_started, {1})))); - ASSERT_THAT(consumer_profile.process_output_latency(), - Partially(EqualsProto(CreateTimeHistogram( - /*total=*/2000 + 250 - when_source_started, {1})))); - ASSERT_EQ(consumer_profile.input_stream_profiles().size(), 2); - // For "stream_0" should have not changed since it was empty. - ASSERT_THAT(consumer_profile.input_stream_profiles(0).latency(), - Partially(EqualsProto(CreateTimeHistogram(/*total=*/0, {0})))); - // For "stream_1" - ASSERT_THAT(consumer_profile.input_stream_profiles(1).latency(), - Partially(EqualsProto(CreateTimeHistogram( - /*total=*/2000 - when_source_finished, {1})))); + + // process input latency total = 2000 (end) - 1000 (when source started) = + // 1000 process output latency total = 2000 (end) + 250 - 1000 (when source + // started) = 1250 For "stream_0" should have not changed since it was empty. + // For "stream_1" = 2000 (end) - 1250 (when source finished) = 850 + EXPECT_THAT(consumer_profile, Partially(EqualsProto(R"( + name: "consumer_calc" + process_input_latency { total: 1000 } + process_output_latency { total: 1250 } + input_stream_profiles { + name: "stream_0" + latency { total: 0 } + } + input_stream_profiles { + name: "stream_1" + latency { total: 850 } + } + )"))); // Check packets_info_ map for PacketId({"stream_1", 100}) should not yet be // garbage collected. diff --git a/mediapipe/framework/profiler/trace_buffer.h b/mediapipe/framework/profiler/trace_buffer.h index 167bc2a89..c435d0d52 100644 --- a/mediapipe/framework/profiler/trace_buffer.h +++ b/mediapipe/framework/profiler/trace_buffer.h @@ -39,9 +39,20 @@ inline const void* GetPacketDataId(const HolderBase* holder) { struct TraceEvent { using EventType = GraphTrace::EventType; // GraphTrace::EventType constants, repeated here to match GraphProfilerStub. - static const EventType UNKNOWN, OPEN, PROCESS, CLOSE, NOT_READY, - READY_FOR_PROCESS, READY_FOR_CLOSE, THROTTLED, UNTHROTTLED, CPU_TASK_USER, - CPU_TASK_SYSTEM, GPU_TASK, DSP_TASK, TPU_TASK; + static constexpr EventType UNKNOWN = GraphTrace::UNKNOWN; + static constexpr EventType OPEN = GraphTrace::OPEN; + static constexpr EventType PROCESS = GraphTrace::PROCESS; + static constexpr EventType CLOSE = GraphTrace::CLOSE; + static constexpr EventType NOT_READY = GraphTrace::NOT_READY; + static constexpr EventType READY_FOR_PROCESS = GraphTrace::READY_FOR_PROCESS; + static constexpr EventType READY_FOR_CLOSE = GraphTrace::READY_FOR_CLOSE; + static constexpr EventType THROTTLED = GraphTrace::THROTTLED; + static constexpr EventType UNTHROTTLED = GraphTrace::UNTHROTTLED; + static constexpr EventType CPU_TASK_USER = GraphTrace::CPU_TASK_USER; + static constexpr EventType CPU_TASK_SYSTEM = GraphTrace::CPU_TASK_SYSTEM; + static constexpr EventType GPU_TASK = GraphTrace::GPU_TASK; + static constexpr EventType DSP_TASK = GraphTrace::DSP_TASK; + static constexpr EventType TPU_TASK = GraphTrace::TPU_TASK; absl::Time event_time; EventType event_type = UNKNOWN; bool is_finish = false; diff --git a/mediapipe/framework/profiler/trace_builder.cc b/mediapipe/framework/profiler/trace_builder.cc index e609e6dcb..197472b32 100644 --- a/mediapipe/framework/profiler/trace_builder.cc +++ b/mediapipe/framework/profiler/trace_builder.cc @@ -385,21 +385,21 @@ void TraceBuilder::CreateLog(const TraceBuffer& buffer, absl::Time begin_time, } void TraceBuilder::Clear() { impl_->Clear(); } -// Defined here since inline constants fail to link in android builds. -const TraceEvent::EventType // - TraceEvent::UNKNOWN = GraphTrace::UNKNOWN, - TraceEvent::OPEN = GraphTrace::OPEN, - TraceEvent::PROCESS = GraphTrace::PROCESS, - TraceEvent::CLOSE = GraphTrace::CLOSE, - TraceEvent::NOT_READY = GraphTrace::NOT_READY, - TraceEvent::READY_FOR_PROCESS = GraphTrace::READY_FOR_PROCESS, - TraceEvent::READY_FOR_CLOSE = GraphTrace::READY_FOR_CLOSE, - TraceEvent::THROTTLED = GraphTrace::THROTTLED, - TraceEvent::UNTHROTTLED = GraphTrace::UNTHROTTLED, - TraceEvent::CPU_TASK_USER = GraphTrace::CPU_TASK_USER, - TraceEvent::CPU_TASK_SYSTEM = GraphTrace::CPU_TASK_SYSTEM, - TraceEvent::GPU_TASK = GraphTrace::GPU_TASK, - TraceEvent::DSP_TASK = GraphTrace::DSP_TASK, - TraceEvent::TPU_TASK = GraphTrace::TPU_TASK; +// Defined here since constexpr requires out-of-class definition until C++17. +const TraceEvent::EventType // + TraceEvent::UNKNOWN, // + TraceEvent::OPEN, // + TraceEvent::PROCESS, // + TraceEvent::CLOSE, // + TraceEvent::NOT_READY, // + TraceEvent::READY_FOR_PROCESS, // + TraceEvent::READY_FOR_CLOSE, // + TraceEvent::THROTTLED, // + TraceEvent::UNTHROTTLED, // + TraceEvent::CPU_TASK_USER, // + TraceEvent::CPU_TASK_SYSTEM, // + TraceEvent::GPU_TASK, // + TraceEvent::DSP_TASK, // + TraceEvent::TPU_TASK; } // namespace mediapipe diff --git a/mediapipe/framework/tool/tag_map.h b/mediapipe/framework/tool/tag_map.h index bdc250924..e2ec97599 100644 --- a/mediapipe/framework/tool/tag_map.h +++ b/mediapipe/framework/tool/tag_map.h @@ -127,6 +127,11 @@ class TagMap { std::vector names_; }; +// Equal TagData structs define equal id ranges. +inline bool operator==(const TagMap::TagData& d1, const TagMap::TagData& d2) { + return d1.id == d2.id && d1.count == d2.count; +} + } // namespace tool } // namespace mediapipe diff --git a/mediapipe/framework/tool/template_expander.cc b/mediapipe/framework/tool/template_expander.cc index 2597dd597..e2de6e3e7 100644 --- a/mediapipe/framework/tool/template_expander.cc +++ b/mediapipe/framework/tool/template_expander.cc @@ -567,6 +567,10 @@ class TemplateExpanderImpl { result = AsDict(args); } else if (expr.op() == "list") { result = AsList(args); + } else if (expr.op() == "size") { + return AsArgument(static_cast( + args[0].has_dict() ? args[0].mutable_dict()->arg_size() + : args[0].mutable_element()->size())); } return result; } diff --git a/mediapipe/framework/tool/template_parser.cc b/mediapipe/framework/tool/template_parser.cc index 62380bf19..2954566e8 100644 --- a/mediapipe/framework/tool/template_parser.cc +++ b/mediapipe/framework/tool/template_parser.cc @@ -1318,8 +1318,8 @@ bool IsInfixOperator(const std::string& token) { // A function-style operator, including a for or if expression. bool IsFunctionOperator(const std::string& token) { static auto kTokens = new std::set{ - "min", "max", "for", "if", "!", - "concat", "lowercase", "uppercase", "dict", "list", + "min", "max", "for", "if", "!", "concat", + "lowercase", "uppercase", "size", "dict", "list", }; return kTokens->count(token) > 0; } diff --git a/mediapipe/gpu/gl_simple_shaders.h b/mediapipe/gpu/gl_simple_shaders.h index 3fed608ad..8bc612ddd 100644 --- a/mediapipe/gpu/gl_simple_shaders.h +++ b/mediapipe/gpu/gl_simple_shaders.h @@ -101,6 +101,10 @@ static const GLfloat kBasicTextureVertices[] = { 1.0f, 1.0f, // top right }; +// Places a texture on kBasicSquareVertices, flipped horizontally. +static const GLfloat kBasicTextureVerticesFlipX[] = { + V4(kBasicTextureVertices, 1, 0, 3, 2)}; + // Places a texture on kBasicSquareVertices, flipped vertically. static const GLfloat kBasicTextureVerticesFlipY[] = { V4(kBasicTextureVertices, 2, 3, 0, 1)}; diff --git a/mediapipe/graphs/youtube8m/BUILD b/mediapipe/graphs/youtube8m/BUILD index be0fff44c..c697d16c0 100644 --- a/mediapipe/graphs/youtube8m/BUILD +++ b/mediapipe/graphs/youtube8m/BUILD @@ -44,3 +44,30 @@ cc_library( "//mediapipe/calculators/video:opencv_video_decoder_calculator", ], ) + +cc_library( + name = "yt8m_inference_calculators_deps", + deps = [ + "//mediapipe/calculators/core:concatenate_vector_calculator", + "//mediapipe/calculators/core:dequantize_byte_array_calculator", + "//mediapipe/calculators/core:packet_cloner_calculator", + "//mediapipe/calculators/core:side_packet_to_stream_calculator", + "//mediapipe/calculators/core:string_to_int_calculator", + "//mediapipe/calculators/tensorflow:lapped_tensor_buffer_calculator", + "//mediapipe/calculators/tensorflow:string_to_sequence_example_calculator", + "//mediapipe/calculators/tensorflow:tensor_to_vector_float_calculator", + "//mediapipe/calculators/tensorflow:tensorflow_inference_calculator", + "//mediapipe/calculators/tensorflow:tensorflow_session_from_saved_model_calculator", + "//mediapipe/calculators/tensorflow:tfrecord_reader_calculator", + "//mediapipe/calculators/tensorflow:unpack_media_sequence_calculator", + "//mediapipe/calculators/tensorflow:unpack_yt8m_sequence_example_calculator", + "//mediapipe/calculators/tensorflow:vector_float_to_tensor_calculator", + "//mediapipe/calculators/tensorflow:vector_int_to_tensor_calculator", + "//mediapipe/calculators/util:annotation_overlay_calculator", + "//mediapipe/calculators/util:labels_to_render_data_calculator", + "//mediapipe/calculators/util:local_file_contents_calculator", + "//mediapipe/calculators/util:top_k_scores_calculator", + "//mediapipe/calculators/video:opencv_video_decoder_calculator", + "//mediapipe/calculators/video:opencv_video_encoder_calculator", + ], +) diff --git a/mediapipe/graphs/youtube8m/label_map.txt b/mediapipe/graphs/youtube8m/label_map.txt new file mode 100644 index 000000000..8321ec772 --- /dev/null +++ b/mediapipe/graphs/youtube8m/label_map.txt @@ -0,0 +1,3862 @@ +Game +Video game +Vehicle +Concert +Musician +Cartoon +Performance art +Car +Dance +Guitar +String instrument +Food +Association football +Musical ensemble +Music video +Animal +Animation +Motorsport +Pet +Racing +Recipe +Mobile phone +Cooking +Smartphone +Gadget +Trailer (promotion) +Toy +Minecraft +Drum kit +Cuisine +Motorcycle +Piano +Dish (food) +Drum +Acoustic guitar +Action-adventure game +Call of Duty +Electric guitar +Drummer +Cosmetics +Keyboard instrument +Choir +Strategy video game +Fishing +Aircraft +Train +Airplane +Pianist +Sports car +Art +Hair +Rail transport +Basketball +Cycling +Orchestra +Motorcycling +Transport +Musical keyboard +Bicycle +Fish +Outdoor recreation +Disc jockey +Machine +Sports game +Radio-controlled model +Hairstyle +Fashion +Dog +Skateboarding +Fighting game +Basketball moves +Wedding +Skateboard +IPhone +Personal computer +Truck +Boat +Railroad car +Snare drum +American football +Drawing +Pokémon +Winter sport +Tractor +Naruto +Grand Theft Auto V +Cymbal +Horse +House +Festival +Engine +Highlight film +Boxing +World of Warcraft +Call of Duty: Black Ops II +Four-wheel drive +Bird +Violin +Skateboarding trick +Christmas +Weight training +Recreational fishing +Warcraft +Ice skating +Driving +Video game console +Microsoft Windows +Airline +Pokémon (video game series) +Landing +Combat +League of Legends +Vegetable +Model aircraft +Airliner +Samsung Galaxy +Sport utility vehicle +Electronic keyboard +Hockey +Radio-controlled aircraft +??? +Eye shadow +Cooking show +Dessert +Battlefield (series) +Slam dunk +Plant +Painting +Drifting (motorsport) +Rallying +Lego +Tablet computer +Call of Duty: Modern Warfare 2 +Comedy (drama) +Grand Theft Auto: San Andreas +Off-road vehicle +The Walt Disney Company +Locomotive +Takeoff +RuneScape +Puppy +Amusement park +Call of Duty: Modern Warfare 3 +Motocross +Dragon Ball +Airport +Photography +Call of Duty: Black Ops +Shoe +Radio-controlled car +Sonic the Hedgehog +Skatepark +Bride +First-person shooter +Accordion +Jet aircraft +Mascara +Halo (series) +Camera +Final Fantasy +Skiing +Gym +Aviation +Mountain bike +Marching band +??? +Extreme sport +FIFA 15 +Brass instrument +Sasuke Uchiha +Cat +Sedan (automobile) +Pickup truck +Meat +BMW +Parade +Cake +Supercar +Aquarium +Weather +Weapon +Nail (anatomy) +Surfing +PlayStation 3 +Room +Call of Duty 4: Modern Warfare +Helicopter +Laptop +Saxophone +Star Wars +Goku +Hotel +Xbox 360 +Arcade game +Doll +News presenter +Exhaust system +Volkswagen +Hatchback +Action figure +Computer +Carnival +Lipstick +Wii +Sonic the Hedgehog (character) +School +Ballet +Eye liner +Heavy equipment +IPad +Running +Baking +Rapid transit +Coupé +Road bicycle +Card game +Nail polish +Playing card +Bus +Counter-Strike (video game) +Gardening +Outline of meals +Nail art +Tank +??? +Bollywood +Tennis +Ship +BMX bike +Drink +Grand Theft Auto IV +Snowboarding +Mountain biking +Rouge (cosmetics) +Super Smash Bros. +??? +Street Fighter +Stadium +Underwater +Hunting +Kickflip +Metin2 +The Sims +Viola +Pony +PlayStation 4 +Television +??? +Beach +Manicure +Chocolate +Wood +Snow +Sneakers +??? +Roller coaster +Afro-textured hair +Timbales +Need for Speed +Robot +Paper +Gymnastics +Farm +Diatonic button accordion +Fighter aircraft +Sketch (drawing) +Mercedes-Benz +Chevrolet +Batman +Loudspeaker +Tool +Nike, Inc. +Race track +Ski +Underwater diving +Computer hardware +Garden +Paint +Cello +Digital camera +Scooter (motorcycle) +Motorboat +Harry Potter +??? +GoPro +Assassin's Creed +Fishing rod +Battlefield 3 +IPod +Nature +Dota 2 +Tree +My Little Pony +Dress +Xbox One +Train station +Firefighter +Jeep +Rail transport modelling +Resort +Flute +Touhou Project +Fruit +Chicken as food +Knife +Dashcam +Clash of Clans +Kitchen +Slide show +The Legend of Zelda +Fireworks +Swimming pool +Rugby football +Building +Kitten +Television advertisement +??? +Battlefield 4 +Horse racing +MapleStory +Subwoofer +Flour +IPod Touch +World of Tanks +Music festival +Comedian +Figurine +Kingdom Hearts +Manga +Wrestling +Trumpet +Xbox +Model (person) +Jumping +Dough +FIFA 13 +Pro Evolution Soccer +Resident Evil +Eye +Guitar Hero +Enduro +Home appliance +News program +Watch +Audi +Off-road racing +Ice dancing +Construction +Organ (music) +PlayStation Portable +Figure skating +Fiddle +WWE 2K +Climbing +Spider-Man +Braid +Muscle +The Elder Scrolls V: Skyrim +Nintendo 3DS +Fire +Human swimming +BMW Motorrad +One Piece +Wildlife +Apartment +Dressage +Scuba diving +Call of Duty: Ghosts +Eating +Kickboxing +Egg as food +Origami +The Elder Scrolls +Ford Mustang +Fishing lure +Light +Running back +Air force +M.U.G.E.N +Transformers +Living room +Soldier +Bag +Ballroom dance +Gohan +Kayak +Sheet music +Destiny (video game) +Wall +Church (building) +Sewing +Chipmunk +Surfboard +Concealer +Drag racing +Mega Man +Walt Disney World +Chicken +Parachuting +Classic car +Furniture +Jewellery +Recreational vehicle +Call of Duty: Advanced Warfare +Street Fighter IV +Sakura Haruno +Restaurant +Halo 3 +Wheelie +Mario Kart +Headphones +Factory +Yu-Gi-Oh! Trading Card Game +Speedometer +Circus +Muscle car +Bedroom +Tekken +Graffiti +River +Lighting +Guitar amplifier +Knitting +Call of Duty: Zombies +PlayStation +Radio-controlled helicopter +Cookware and bakeware +Trail +Camping +University +Indian cuisine +Multiplayer online battle arena +Ball +Nightclub +Book +Lego minifigure +PlayStation 2 +Dodge +Garry's Mod +Camera lens +Hockey puck +Barbie +Thomas the Tank Engine +Go-kart +Vegetarian cuisine +Monster High +Yacht +Collectible card game +Auto Race (Japanese sport) +Role-playing game +Madden NFL +Unidentified flying object +Longboard (skateboard) +Toddler +Digital single-lens reflex camera +Xbox (console) +Rail freight transport +Honda Civic +Convertible +The Sims 2 +Lamborghini +Printer (computing) +Cream +Parrot +Tire +Quadcopter +Littlest Pet Shop +Wii U +Planet +??? +The Sims 3 +Sony Xperia +Salad +Sailboat +Cruise ship +Unmanned aerial vehicle +Naruto: Ultimate Ninja +Barbecue +Mortal Kombat +Slot machine +Longboarding +Halo: Reach +Paragliding +Bread +Monster Hunter +Stitch (textile arts) +Dofus +StarCraft II: Wings of Liberty +Game controller +Gears of War +Mud bogging +Snowboard +Synthesia +Wig +Road bicycle racing +Wheel +Macintosh +Home improvement +Printing +Insect +Road +Parachute +Cattle +Hair coloring +IPhone 4S +Advertising +Potato +Runway +Van +Zoo +Handheld game console +Water +Rock Band +Volkswagen Golf +Bathroom +Stunt performer +Bleach (manga) +Metal Gear +Santa Claus +Hiking +Samsung Electronics +Runway (fashion) +Elevator +Cricket +Gran Turismo (series) +Fire engine +Kinder Surprise +Play-Doh +Grilling +Eyelash +Table tennis +Fiat Automobiles +Dragon +Lion +Nintendo Entertainment System +PlayStation (console) +Stallion +Ice skate +Baseball park +Flamenco +Steam engine +Plough +Farming Simulator +Soup +Snowmobile +Mare +Counter-Strike: Source +Sail +Squat (exercise) +Bass (fish) +Banjo +Harmonica +Quartet +Drum stick +IPhone 5 +Reptile +Prayer +T-shirt +Talent show +Rice +Roasting +Diablo III +CrossFire (video game) +Renault +Pizza +Trombone +Chevrolet Camaro +Barbell +Ryu (Street Fighter) +Clay +Beyblade +Lake +Sauce +??? +Cube +Forza (series) +Cookie +Taiko no Tatsujin +Mixtape +Medicine +Door +Monster +Call of Duty: World at War +Mud +Computer keyboard +Clarinet +Defense of the Ancients +Sora (Kingdom Hearts) +Computer monitor +Super Street Fighter IV +PlayStation Vita +Guild Wars +Album +Model car +Tenor saxophone +The Twilight Saga (film series) +Rubik's Cube +Sailor Moon +Teacher +Mixing console +Card manipulation +Combine harvester +Boeing 737 +Bull +Fish as food +Cheese +Concrete +Board game +Moped +Puzzle +Lego Star Wars +Poker +Portrait +Luigi +Dining room +Pokémon X and Y +Floor +Asus +Inuyasha +Livestock +Lawn mower +Tibia (video game) +Tabletop game +Iron Man +Tomato +Juice +Final Fantasy VII +Lip gloss +Super Smash Bros. Melee +Central processing unit +Sitcom +Cockpit +Emergency vehicle +FIFA 12 +Bodyboarding +Earth +The Lego Group +Ice cream +Microphone +Rallycross +Website +Table (furniture) +Ice +Magic: The Gathering +Ninja +Darth Vader +Saw +Mickey Mouse +Handbag +The King of Fighters +Ballet dancer +Samsung Galaxy Note series +Washing machine +Zee TV +Point Blank (2008 video game) +Gibson Les Paul +Dune buggy +DayZ (video game) +Television set +Dirt track racing +Edward Cullen +Beauty salon +Hetalia: Axis Powers +Vampire +Gliding +Batman: Arkham +Mountain +Rain +Shark +Waterfall +DarkOrbit +Bagpipes +Comics +Rock climbing +Skin +Arena +IPhone 4 +ARMA (series) +Super Smash Bros. for Nintendo 3DS and Wii U +Curry +Pasta +Halo 4 +Superman +Icing (food) +Google Nexus +Marathon +Deer +Guitar Hero III: Legends of Rock +Balloon +Goalkeeper (association football) +Red Bull +Nissan GT-R +Noodle +Fishing bait +Pencil +Plants vs. Zombies +Athlete +Computer case +Stretching +Terrier +Outer space +Textile +Mercedes-AMG +Hard disk drive +Biceps +Handball +Land Rover +Kamen Rider Series +Parakeet +Bear +Rim (wheel) +Chevrolet Corvette +Battery (electricity) +Milk +Roblox +BMW M3 +Christmas decoration +Moon +Microsoft Lumia +Combat Arms (video game) +Maize +Cargo +Headset (audio) +Bee +Helmet +Street art +Clown +Tattoo +Cupcake +Traxxas +Money +Hatsune Miku: Project DIVA +Bead +Angry Birds +Movieclips +Optimus Prime +MacBook +Mass Effect +Bowser (character) +Sega Genesis +Pachinko +Jedi +Jeep Wrangler +Dragon Ball Z: Budokai Tenkaichi +Tales (series) +Loader (equipment) +Water park +Beef +Sewing machine +Beer +Glass +Silage +Seafood +Gran Turismo 5 +Harp +Joker (comics) +Volkswagen Beetle +??? +BlackBerry +AdventureQuest Worlds +Bowling +Guild Wars 2 +Dragon Quest +Washing +Mermaid +Cue stick +Boot +Stir frying +Grand Theft Auto: Vice City +Penguin +Acrylic paint +Cocktail +Kingdom Hearts II +Coral +Borderlands 2 +Telephone +Gears of War (video game) +Far Cry +Tractor pulling +Rock Band (video game) +Crane (machine) +Updo +Stuffed toy +Lawn +Tekken (video game) +Airbus A320 family +IPhone 5S +Watercolor painting +Ten-pin bowling +Duck +Pokémon Trading Card Game +Oven +Subaru Impreza +Porsche 911 +Backpack +Carl Johnson (Grand Theft Auto) +German Shepherd +Turtle +Metal +Left 4 Dead +Ultralight aviation +Comic book +Batting (cricket) +Tram +Mower +Reef aquarium +??? +Swing (dance) +Lego City +Game Boy Advance +Diesel engine +Pitcher +Dance studio +Hamburger +Cake decorating +Left 4 Dead 2 +Bible +Candy +Vacuum cleaner +Pokémon Omega Ruby and Alpha Sapphire +Sowing +Roof +Donkey Kong +Trout +Coin +Tent +Digimon +Costume +Warface +Sandwich +BMW 3 Series +Star Wars: The Old Republic +Trampoline +Pipe organ +Latin dance +Aerobics +Aion: Upheaval +Supermoto +Netbook +Gift +Strum +Mitsubishi Lancer Evolution +Drum and bugle corps (modern) +Gramophone record +Gundam (mobile suit) +Euro Truck Simulator 2 +Tai chi +Teenage Mutant Ninja Turtles +Aerobatics +Wedding dress +Hair conditioner +Achievement (video gaming) +Boeing 777 +Shadow the Hedgehog +Boeing 747 +Simba +Silkroad Online +Kindergarten +Smartwatch +Computer mouse +Bell +Museum +Rabbit +Total War (series) +DVD +Devil May Cry +Face +Lathe +Five Nights at Freddy's +Logging +String quartet +Bridge +Super Mario Bros. +Fishing reel +Badminton +Clock +Stove +Wine +Subaru +Leather +IPad 2 +Terraria +Attack on Titan +Bottle +Kick +Police officer +Raw foodism +Video card +Alpine skiing +String (music) +StarCraft (video game) +Roadster (automobile) +Steak +Hearthstone (video game) +Solo dance +Foreign exchange market +God of War (series) +Hulk (comics) +Easter egg +Ceiling +Yo-kai Watch +Wakeboarding +Monster truck +McDonald's +Assassin's Creed III +Chopper (motorcycle) +Largemouth bass +Roller skating +Glider (aircraft) +Jacket +Marimba +Christmas tree +Sand +Afro +MacBook Pro +Booster pack +Dark Souls II +Bartender +Quarterback +Illustration +ARMA 2 +Star Trek +Itachi Uchiha +Hot rod +Saints Row +Freeza +Need for Speed: Most Wanted (2012 video game) +Hair twists +Super Mario World +Crash Bandicoot +Pork +Shampoo +Mask +Hair iron +Marvel vs. Capcom +Castlevania +Halo 2 +Battery charger +Tower defense +BBC +Kawasaki motorcycles +Link (The Legend of Zelda) +Muffler +Nintendo 64 +Marriage proposal +Fingerboard (skateboard) +Beehive +Pokémon HeartGold and SoulSilver +Bowling ball +Tower of Saviors +Artificial nails +Final Fantasy XIII +Chair +Hijab +Juggling +Nissan Skyline +Anpanman +Car wash +Kite +Diablo (video game) +Resident Evil 4 +Candy Crush Saga +Rocket +Video game arcade cabinet +Whale +Glider (sailplane) +Flooring +Kingdom Hearts (video game) +??? +Fast food +Mandolin +Metal detector +Cinema 4D +Ash Ketchum +Router (computing) +Yamaha YZF-R1 +Uncharted +DC Comics +Egg +Lexus +Ollie (skateboarding) +Hamster +Chainsaw +Galaxy +Embroidery +Suite (hotel) +Brush +Electronic drum +Gran Turismo 6 +NBA 2K15 +Dolphin +Salmon +Window +Drill +Pen +Backpacking (wilderness) +Torte +Web page +Dreadlocks +Hot Wheels +Brake +Tuba +Volcano +Ibiza +Dragon Age +Mini +Perfect World (video game) +Knot +Tails (character) +Thunderstorm +Video camera +Smoothie +Crossover (automobile) +Condominium +Desert +Pump +Strawberry +Coffeemaker +The Legend of Zelda: Ocarina of Time +Tarot +Architecture +Portal (video game) +Dynasty Warriors +Lightning McQueen +Pirates of the Caribbean (film series) +Tile +Battlefield: Bad Company 2 +Sketch comedy +Aikido +V8 engine +Sailor Moon (character) +Lamborghini Aventador +Carp fishing +Kirby (series) +Banana +Police car +Laser lighting display +Necklace +??? +WWE '13 +Mini (marque) +Tanki Online +Oil +Radio-controlled boat +Dinosaur +Pie +President of the United States +NBA 2K14 +Labrador Retriever +Blender +Plarail +Captain America +Electric locomotive +Street racing +Need for Speed: Most Wanted (2005 video game) +Canoe +Golf club +Sheep +Bar +CDJ +Lace +Gold +Glove +Halo: Combat Evolved +Alphabet +Fender Telecaster +IPhone 3GS +Beadwork +Personal water craft +Dietary supplement +James Bond +Ragnarok Online +French braid +Road racing +Star +Dean Winchester +Snake +Seed +Christmas lights +Plaster +Trunks (Dragon Ball) +Forage harvester +Cartoon Network +Honda CBR series +Battlefield Hardline +Tekken 6 +Glitter +Ford Focus +Roland V-Drums +Ski-Doo +Tyrannosaurus +New Super Mario Bros. +Cue sports +Rainbow Loom +Samsung Galaxy S III +Glasses +Italian cuisine +RollerCoaster Tycoon 3 +Pig +Lock (security device) +The Lord of the Rings (film series) +Military parade +Elephant +Pull-up (exercise) +Eyelash extensions +Ring (jewellery) +Minivan +Coca-Cola +Mural +Love song +Portal 2 +Mortal Kombat (2011 video game) +Yarn +Pokémon Ruby and Sapphire +Dragon Nest +Japanese cuisine +Resident Evil 5 +Jeans +Map +Pikachu +Sun +Pond +Bulldog +Greenhouse +Škoda Auto +Baby transport +Apple +The Doctor (Doctor Who) +Turbine +Naruto: Ultimate Ninja Storm +Watch Dogs +VHS +Ariel (Disney) +Sculpture +Bulldozer +Transformice +Sushi +Home run +Fountain +Slopestyle +Fullmetal Alchemist +Ultimate Marvel vs. Capcom 3 +Automotive lighting +Lightsaber +Chevrolet Silverado +Honey +Wangan Midnight +Sword +Toilet +Super Mario Galaxy +Akuma (Street Fighter) +Shiva +Bed +Toy train +Manufacturing +Ram Trucks +Stuffing +Biscuit +Kia Motors +Spa +Samsung Galaxy S II +Demolition +Airbus A330 +Breakfast +Airbus A380 +Pancake +Kawasaki Ninja +Mitsubishi Lancer +Mushroom +Grand Theft Auto: The Lost and Damned +Microsoft Flight Simulator +Spacecraft +Logo +Stock car racing +Goat +Pool (cue sports) +Assassin's Creed (video game) +Majin Boo +Vespa +??? +Samsung Galaxy S4 +Assassin's Creed IV: Black Flag +Batman: Arkham City +Monkey +Death Note +WWE 2K15 +Pumpkin +Shopping mall +Rose +Cola +Minnie Mouse +Caporales +Jet Ski +World of Warcraft: Wrath of the Lich King +Winter +Prom +Karaoke box +Minibike +RFactor +Art exhibition +Plush +Chocolate cake +Ford F-Series +Soap +Knuckles the Echidna +Dump truck +Giant panda +Dance Dance Revolution +Princess +Street food +Flashlight +Animal Crossing +Pilates +Pipe band +Toyota Land Cruiser +Lara Croft +Jumbotron +Ferrari F430 +Cell (Dragon Ball) +BMW 3 Series (E36) +Injustice: Gods Among Us +Dumbbell +Samsung Galaxy Tab series +Bodyweight exercise +Penalty kick (association football) +Lizard +City +Bionicle +Kirby (character) +WWE 2K14 +Pokémon Battle Revolution +Sonic the Hedgehog (1991 video game) +Alliance of Valiant Arms +Racket (sports equipment) +K-1 +Acer Inc. +Recorder (musical instrument) +Earring +National park +The Elder Scrolls IV: Oblivion +Audi R8 +Clothes dryer +Military band +Silver +Warcraft III: Reign of Chaos +Classroom +Samsung Galaxy S5 +Black cat +Scarf +Kratos (God of War) +Skylanders +Super Robot Wars +Electric car +Video lesson +Smoking (cooking) +Antenna (radio) +Sonic Generations +Butter +Chess +Hello Kitty +Goldfish +Carrot +Blu-ray +Squirrel +Balloon (aeronautics) +Microwave oven +Range Rover +Wool +TalesRunner +IPad Mini +Pokémon Emerald +Inflatable boat +Bull riding +Football boot +Gears of War 2 +Bugatti Veyron +Airbrush +Brick +Avengers (comics) +Plants vs. Zombies 2: It's About Time +United States Navy +Ball (association football) +Volkswagen Gol +Yo-yo +Forza Motorsport 4 +Logitech +Shirt +Golden Retriever +Alarm device +Water slide +Paramotor +Fondant icing +Acrobatic gymnastics +Coach (sport) +The Witcher 3: Wild Hunt +Tabla +Kinect +Zee Bangla +??? +Cabinetry +Quilt +Claw crane +Spyro (series) +Yoshi +Tekken Tag Tournament 2 +Diamond +Samsung Galaxy S series +BMW 3 Series (E46) +Tiger +Number +Traffic +Metalworking +Haruhi Suzumiya +Gown +Luxury yacht +Yuna (Final Fantasy) +Station wagon +Softball +The Legend of Zelda: Twilight Princess HD +Dungeon Fighter Online +Plasticine +LG Optimus series +Source (game engine) +Battlefield 2 +BMW 3 Series (E30) +Ink +Half-Life 2 +Hitman (series) +Inline skates +Remote control +Mercedes-Benz C-Class +The Sims 4 +Harlem Shake (meme) +Magic Kingdom +Dune +Prince of Persia +Final Fantasy XIV +Marvel Universe +Draco Malfoy +Ram Pickup +DC Universe Online +Assassin's Creed II +Mars +Xylophone +Dragon Age: Inquisition +Game Boy +Carpet +Roxas (Kingdom Hearts) +Balance beam +Mass Effect 2 +Dragon Ball Xenoverse +Call of Duty: Black Ops – Zombies +Cadillac +Guinea pig +The Hobbit (film series) +Need for Speed: World +Pastry +Chapel +Rayman +Armour +Mouse +Assassin's Creed: Brotherhood +Lord Voldemort +Magnet +The Sims (video game) +Rubber band +Grocery store +Reborn doll +Ford GT +WWE '12 +PlanetSide 2 +Jaguar Cars +Volvo Cars +Jeep Cherokee (SJ) +Homer Simpson +USB flash drive +Torero +Persona (series) +Model railroad layout +Buttercream +Serve (tennis) +Ferrari 458 +Honda Accord +Chevrolet Impala +Command & Conquer +Warframe +Chrysler (brand) +Standup paddleboarding +Pretty Cure +Campsite +Final Fantasy VIII +Audi A4 +Sailing ship +Rafting +Custom car +Belle (Disney) +Rowing (sport) +Jeep Grand Cherokee +Wire +BMW M5 +Hula hoop +Pinball +Spaghetti +Monster Hunter Freedom Unite +Far Cry 4 +Pro Evolution Soccer 2015 +Test Drive (series) +Motorcycle helmet +Router (woodworking) +Cave +Cheesecake +Birthday cake +Suzuki Jimny +New Super Mario Bros. Wii +Ezio Auditore da Firenze +Fisherman +Mime artist +Roller skates +Pump It Up (video game series) +Dissidia Final Fantasy +Supercharger +Gemstone +Titanfall +Downhill +Medal +Garbage truck +Forehand +Heroes of Newerth +Plastic +??? +Astronaut +Guitar Hero World Tour +ArcheAge +Lowrider +Police dog +Toyota Corolla +Ford Fiesta +Helmet camera +Cabal Online +Assassin's Creed Unity +Ceramic +Kidō Senshi Gundam: Senjō no Kizuna +Hot air balloon +Shower +Donald Duck +Multi Theft Auto +Rock Band 3 +Porsche 911 GT3 +Stick figure +Sled +Lemon +Frog +Mexican Creole hairless pig +Forklift +Dog agility +Kettlebell +Shelby Mustang +Candle +Bowling (cricket) +Kick (football) +Electric vehicle +Oboe +Desktop computer +Wing Chun +Statue +DayZ (mod) +Eagle +Fire station +Nike Air Max +Rage (video game) +Woodturning +Fireplace +Volkswagen Jetta +Madison Square Garden +Fly tying +Spore (2008 video game) +Hammond organ +Sam Winchester +The Pink Panther +Saints Row: The Third +Cherry blossom +Doraemon +WWE action figures +Marvel vs. Capcom 3: Fate of Two Worlds +Bugatti Automobiles +Fire Emblem +Border Collie +Aircraft carrier +Snow blower +Culinary art +Ken Masters +Seafight +Sport bike +Dentist +Easter egg (media) +Joystick +Tuna +Crysis 2 +Audi Quattro +Academy Awards +Ponytail +Ramen +Hummer +Fishing tackle +Final Fantasy X-2 +Coupon +Porsche Carrera +Wood carving +Rocksmith +Wallet +Refrigerator +Koi +Battlefield Heroes +Phonograph +Onion +Biceps curl +Trainz +Hat +Jubeat +Nissan Skyline GT-R +Mattel +GameCube +LittleBigPlanet 2 +Epiphone +Inazuma Eleven +Soft tennis +Killer whale +Hair straightening +Merienda +The Witcher (video game) +Skate (video game) +Live for Speed +Rooster +Chihuahua (dog) +Triangle +Land Rover Defender +Marvel Legends +Trousers +SD Gundam Capsule Fighter +Ratchet & Clank +Doughnut +Hatsune Miku: Project DIVA F +Bouzouki +Domestic canary +Half-Life (video game) +Raven (comics) +Black Butler +Mario Kart 8 +Chili pepper +BMW 5 Series +Hail +Ouran High School Host Club +Brain +Chinese cuisine +Playmobil +Model building +Ribbon +Pit bike +Sonic Unleashed +Solar panel +Orange (fruit) +Otis Elevator Company +Mu Online +Hang gliding +Path of Exile +Animal Crossing: New Leaf +Steel guitar +Sword Art Online +Lego Ninjago +Paddle +Second Life +Aikatsu! +IPhone 5C +Gothic (series) +Batman: Arkham Asylum +Carburetor +Crab +Espresso machine +The Phantom of the Opera (1986 musical) +Hellsing +Spider +Super Mario Galaxy 2 +Duel Masters Trading Card Game +Drywall +Laundry +United States Air Force +Assassin's Creed: Revelations +Corel +Omelette +Composer +Ford Escort (Europe) +Grape +Honda CB600F +Tea +Elmo +Temple +Need for Speed: Carbon +Catamaran +Perfect World (company) +Skate 3 +Missile +Infomercial +Chevrolet Chevelle +Airport terminal +Crysis (video game) +StepMania +Red Dead Redemption +Atari +Couch +The Idolmaster +Beatmania IIDX +Big wave surfing +Tokyo Mew Mew +Wheat +Warhammer Fantasy Battle +Rock (geology) +Snowplow +Submarine +Doctor Eggman +Wood flooring +Bangs (hair) +Yamaha YZF-R6 +Pontiac Firebird +Red Dead +Field hockey +Vineyard +Waterfowl hunting +Domestic pigeon +Toyota Hilux +CNET +Preacher +Sonic Adventure +Lamborghini Murciélago +Marinera +Screen printing +Crazyracing Kartrider +The Legend of Zelda: Majora's Mask +Sunglasses +Log cabin +Fungus +Wedding photography +Flag +Devil May Cry 4 +Cappuccino +Flamenco guitar +Projector +Rock dove +The Elder Scrolls Online +LittleBigPlanet (2008 video game) +Digital video recorder +Djembe +Vending machine +Mehndi +Telescope +Flyff +Pattern (sewing) +Stairs +Nissan 350Z +Cell (biology) +Need for Speed: Underground 2 +Incandescent light bulb +Gallon +Greeting card +Balloon modelling +Sensor +Realm of the Mad God +Nest +Writing +Logic Pro +Opel Astra +Campervan +Cooked rice +Muffin +Wind power +Hedgehog +Soft drink +Calculator +Harness racing +Buick +Beast (Disney) +Destroyer +Point guard +Forza Horizon +Mercedes-Benz SLS AMG +Supermarket +Catfish +Final Fantasy XI +The Last of Us +Battleship +Dodge Challenger +Peter Pan +Metal Gear Solid 4: Guns of the Patriots +Toyota 86 +Bakery +Compact disc +Backhoe +Saddle +Total Drama Island +Erhu +Bumblebee (Transformers) +Cajón +Beatmania +Ice rink +Child safety seat +Honda S2000 +Samsung Galaxy Note II +Higurashi When They Cry +Union Pacific Railroad +BMW 3 Series (E90) +V6 engine +BlazBlue +Rottweiler +Necktie +Image scanner +White-tailed deer +TV4 (Sweden) +Bishop +Need for Speed: Hot Pursuit (2010 video game) +Princess Peach +Rust (video game) +Doom (1993 video game) +Fender Custom Shop +Smite (video game) +Nissan Silvia +??? +Pudding +Sephiroth (Final Fantasy) +Irish dance +MacBook Air +Commodore 64 +IMac +Space Shuttle +Automobile repair shop +Collie +Dragon Age: Origins +Sangokushi Taisen +Calligraphy +Black belt (martial arts) +??? +Valve +Crisis Core: Final Fantasy VII +Two-stroke engine +Killzone (series) +Full moon +Hunter × Hunter +New York City Subway +Latte +Mercedes-Benz S-Class +Tetris +Samurai +Predator (alien) +Arabian horse +Mercedes-Benz E-Class +Spinach +Dōjinshi +Polar bear +Body piercing +Amazon Kindle +Biology +Key (lock) +Mobile Suit Gundam: Extreme Vs. +Rappelz +Bobber (motorcycle) +Toy balloon +Mexican cuisine +Rope +Taco +Taxicab +Infestation: Survivor Stories +Clutch +PlayStation Network +Garage (residential) +Milkshake +Cloud Strife +Honda Integra +Eintopf +Primary school +Kingdom Hearts Birth by Sleep +Resident Evil (1996 video game) +Foal +GameSpot +Castle +Human hair color +Scorpion (Mortal Kombat) +Poultry +Poodle +Vans +Forza Horizon 2 +Zero (Mega Man) +Toyota Camry +Chemical reaction +Test Drive Unlimited 2 +Bacon +Mario Party +18 Wheels of Steel +Goose +Sausage +Compost +Cucumber +French horn +Analog synthesizer +Siamese fighting fish +??? +Las Vegas Strip +Crysis 3 +School bus +Oculus Rift +Carnival Cruise Line +Honda CBR600RR +Pokémon Red and Blue +Autobot +Christ (title) +Cockatiel +Ace Combat +Mazda MX-5 +Countertop +Safari +Final Fantasy XIV: A Realm Reborn +Track (rail transport) +Ganon +Two-wheel tractor +??? +Watermelon +Paper plane +Rainbow trout +??? +Tony Hawk's (series) +Korean cuisine +Lip balm +Angry Birds (video game) +Lead guitar +Pug +Monster Hunter Tri +Playground +God of War III +Herd +Niko Bellic +Bungee jumping +Soil +Subway Surfers +Hindu temple +Audi A6 +Hogwarts +Eggplant +Mabinogi (video game) +Sugar +Makeup brush +Rocksmith 2014 +Ocean +Asphalt (series) +Dental braces +Bob cut +Nissan 240SX +Cement +Sharpening +Leopard +United States Army +Tom and Jerry +Xbox 360 controller +Dragon Ball: Raging Blast 2 +Winnie the Pooh (franchise) +Trophy +Inazuma Eleven (manga) +Owl +Street Fighter II: The World Warrior +Golf ball +Floyd Mayweather Jr. vs. Manny Pacquiao +Belt (clothing) +Slender: The Eight Pages +Test Drive Unlimited +Super Mario Bros. 3 +Power supply +Retail +Venom (comics) +IPad (3rd generation) +Teddy bear +Denim +Baseball bat +Halo 3: ODST +Train Simulator (Dovetail Games) +Bowhunting +Lotus Cars +Pineapple +Boeing 737 Next Generation +Audi A3 +Dreamcast +City-building game +Diablo II +Suzuki Hayabusa +Gamepad +Electrical wiring +Kitchen stove +Yamaha Aerox +Monster Hunter Portable 3rd +BMX racing +Katara (Avatar: The Last Airbender) +HP Pavilion (computer) +Emirates (airline) +Amiga +Touchscreen +Winter storm +Driver (video game series) +Pac-Man +Fantage +Land Rover Discovery +Flash (photography) +Human back +Intermodal container +Infiniti +Guilty Gear +Animal shelter +Butterfly +Piccolo (Dragon Ball) +Bicycle frame +Boeing 787 Dreamliner +Toontown Online +Renault Mégane +Age of Empires +Canyon +Ski jumping +Lumber +Carousel +Phantasy Star Online 2 +Dodge Viper +Madden NFL 13 +A-18 Hornet +String trimmer +Mattress +Mixer (cooking) +Sub-Zero (Mortal Kombat) +Ford Ranger (North America) +ESPN +ABS-CBN News and Current Affairs +Synchronised swimming +G-Shock +??? +Angel +Champion +Horse show +??? +Rurouni Kenshin +Halo 5: Guardians +Coconut +Deep frying +Dollhouse +Campus +Volkswagen Golf Mk6 +Curtain +Mountain pass +Dojo +Boiler +PRS Guitars +Diesel locomotive +Monster Hunter 4 +French Bulldog +Prince (Prince of Persia) +Fixed-gear bicycle +Ninja Gaiden +Samsung Galaxy Note 3 +Opel Corsa +Jack Sparrow +Boeing 767 +Lexus IS +Tales of Symphonia +Autumn +Inline skating +Filter (aquarium) +Naruto Shippuden: Ultimate Ninja Storm Generations +Garmon +Flower bouquet +SimCity +Gravy +Bully (video game) +French fries +Kawasaki Ninja 250R +Rock fishing +Batman: Arkham Origins +Ceiling fan +Audi TT +Space Marines (Warhammer 40,000) +Acer Aspire +D.Gray-man +Duct tape +Electromagnetic coil +Heroes of the Storm +Tom Clancy's Ghost Recon +Sponge cake +Steelpan +Modem +The King of Fighters 2002 +Dying Light +Need for Speed: Shift +Riot Games +Rainbow +Bean +Chevrolet Opala +Reborn! +Floral design +Megatron +Kawasaki Ninja ZX-6R +Agriculture +Cottage +Television presenter +Metal Gear Solid V: The Phantom Pain +Juicing +BioShock +Plymouth (automobile) +Crêpe +Fist of the North Star +The Legend of Zelda: The Wind Waker +X-Men +Piston +Deck (building) +Nativity scene +Sega Saturn +Stardoll +Just Dance (video game) +Chun-Li +BMW R1200GS +LG G3 +Fisheye lens +Dragon Ball: Raging Blast +Big Boss (Metal Gear) +Dam +Gel +JBL +Dachshund +Bane (comics) +E-reader +The Lord of the Rings Online +Ferb Fletcher +Yeast +Monastery +Vampire Knight +Vodka +IPhone 3G +Tricycle +Metal Slug (series) +Steel +LED lamp +Geometry Dash +Dominoes +Gibson Les Paul Custom +Street Fighter III: 3rd Strike +Hay +Honda CR-X +Spray painting +Flip Video +Bald eagle +God of War II +Clay animation +Tomato sauce +Clone trooper +Beagle +Popcorn +Rubber stamp +Clannad (visual novel) +Fried rice +Moto G (1st generation) +Toyota Prius +Mega Man Battle Network +Doom II: Hell on Earth +Grand Theft Auto: Vice City Stories +Deadpool +Phantasy Star +Lock picking +Sugar paste +Chevrolet Caprice +??? +Herb +The Legend of Zelda: Skyward Sword +Domesticated turkey +Final Fantasy VI +BMW S1000RR +Mitsubishi Pajero +Mazda3 +IKEA +Chevrolet S-10 +Paper Mario +India TV +Tow truck +Orochimaru (Naruto) +Ape +Line (geometry) +Kawasaki Ninja ZX-10R +Aerosol spray +Power supply unit (computer) +Zucchini +Doberman Pinscher +Wolfenstein (series) +Contortion +Fertilizer +Cooler Master +Highway +Chocolate brownie +Street Fighter III +Tsubasa: Reservoir Chronicle +Parking +Olaf (Disney) +Frets on Fire +Multi-function printer +Suzuki GSX-R1000 +Lush (company) +Hang (instrument) +Nexus 7 (2012) +Skyscraper +Gorilla +Ōendan +Puff pastry +Crossbow +Forza Motorsport 5 +Uncharted 2: Among Thieves +Pokémon Mystery Dungeon +Closet +??? +Daytona International Speedway +VTEC +Cheerleading +Slot car +Garden railway +Albert Wesker +Naruto Shippuden: Ultimate Ninja Storm 2 +Sewing needle +Trials (series) +Sheriff Woody +K +Straw +Mitsubishi Eclipse +Frisbee +TrackMania +Manure +Chocolate chip +Cart +Borderlands: The Pre-Sequel +Diving +Wood-burning stove +Medal game +Chrono Trigger +Sherlock Holmes +Library +Volkswagen Golf Mk2 +Guzheng +Malinois dog +Goofy +Pedal steel guitar +Virtua Fighter 5 +Lego Marvel Super Heroes +Kantai Collection +Electric violin +Firewood +Devil May Cry 3: Dante's Awakening +Digital painting +Flair bartending +Boxer (dog) +Melon +Low-carbohydrate diet +Škoda Octavia +The Crew (video game) +Unicycle +GAZ +Gummy bear +Marker pen +Need for Speed: The Run +Dead Space (2008 video game) +Duke Nukem +Dirt 3 +Movie theater +Final Fantasy XIII-2 +Comet +WWE SmackDown vs. Raw 2010 +Gran Turismo 4 +Star Wars: Battlefront II +Lamb and mutton +Ant +Loki (comics) +Percy the Small Engine +Villain +Plumbing +Avocado +BioShock Infinite +Dormitory +Mango +Lucky Star (manga) +Shadow the Hedgehog (video game) +Cabbage +Peanut butter +Didgeridoo +Hard Rock Cafe +Donkey Kong Country +Amazon.com +Star Wars Battlefront (2015 video game) +Harpsichord +Aston Martin Vantage (2005) +Suzuki Swift +Crocodile +Jet engine +Sonic the Hedgehog 2 +Delta Air Lines +Harry Potter and the Deathly Hallows +Trunk (car) +Zangief +Brave Frontier +Chuck E. Cheese's +Iori Yagami +Robotics +Kebab +Cheeseburger +Hatsune Miku: Project DIVA F 2nd +Humbucker +Camcorder +Mega Man X (video game) +Landscape +Shih Tzu +Volkswagen Golf Mk4 +Pollution +Guppy +Coffeehouse +Killer Instinct +Crusher +Allods Online +??? +Boeing 757 +Eclipse +Meatball +Saints Row 2 +Roulette +Grand Theft Auto: Liberty City Stories +Walleye +Walmart +Bearing (mechanical) +Forest +Forever 21 +Canvas +Rat rod +Soulcalibur V +Sonic the Hedgehog (2006 video game) +Multirotor +??? +LG G2 +Moisturizer +Halo: The Master Chief Collection +SEAT León +Skylanders: Swap Force +Pan flute +Chevrolet Tahoe +Metal Gear Online +Fiat 126 +Mount & Blade: Warband +Kennel +Vibraphone +Satellite +Yamaha Raptor 700R +Sonic & Knuckles +Honda Fit +Caridea +Armored Core +Bull Terrier +Firefighting +Catwoman +Octopus +Fencing +Sitar +Limousine +Nintendo DSi +HTC One (M8) +McDonnell Douglas F-15 Eagle +Rat +GoldenEye 007 (1997 video game) +Gasoline +Ken (doll) +Quadracycle +Dead or Alive (series) +Microsoft Surface +Scooby-Doo +Landscape painting +Toyota Land Cruiser Prado +Hair removal +Sink +Mount & Blade +BMW 5 Series (E39) +Mewtwo +Mambo (music) +The Witcher 2: Assassins of Kings +North American P-51 Mustang +Alien (creature in Alien franchise) +Cloud +Forge +Christian Church +Tom Clancy's Rainbow Six +Mirror +Chevrolet Big-Block engine +Chevrolet Corvette (C6) +Abarth +Mazda RX-8 +Pendant +Metal Gear Solid 3: Snake Eater +Buffet +Haunted house +Cockatoo +Royal Air Force +The Embodiment of Scarlet Devil +LG G series +Fishing vessel +DualShock +Sonic Heroes +Drawer (furniture) +BMW 1 Series +Werewolf +DatPiff +Koi pond +Toyota Celica +Twelve-string guitar +Potato chip +Stargate +Killer Instinct (2013 video game) +Caramel +Sprite (computer graphics) +NHL 14 +Ham +Sky +Sweater +Chocolate chip cookie +stay night +Text (literary theory) +Skate 2 +Engraving +Final Fantasy XV +Cornrows +Light Yagami +Floristry +Sly Cooper +Volkswagen Golf Mk5 +Snowman +??? +Vox (musical equipment) +Happy Farm +Orc +Suit (clothing) +PC game +Ace Online +Saints Row IV +Slingshot +Dead Island +Ratchet (Ratchet & Clank) +Gears of War: Judgment +Dragon Quest X +Furby +Crayon Shin-chan +Soprano saxophone +Tifa Lockhart +European perch +Patio +Fried chicken +Sawmill +Mirror's Edge +Canon PowerShot +Guitar Hero: Warriors of Rock +Rome: Total War +Hummer H2 +Radar +Final Fantasy IV +Table saw +Barista +BMW 7 Series +Camel +Windows Media Video +Felt +Audi S4 +Cowboy +Molding (process) +Contact lens +Fiat Punto +The Hobbit +Indoor cycling +Sunset +??? +Persian cat +Hitman: Absolution +Battlefield: Bad Company +Eren Yeager +Sinterklaas +Crash Bandicoot (video game) +Midnight Club: Los Angeles +Metal Gear Rising: Revengeance +Hand-to-hand combat +Avon Products +Log splitter +Stormtrooper (Star Wars) +Epic Rap Battles of History +Shed +Walking +Belt (mechanical) +Hot dog +Sock +Chicken coop +Humpback whale +Character (arts) +Peugeot 106 +Toast +Princess Jasmine +Exercise ball +Fox +Green Lantern +Looney Tunes +Wedding ring +Tap (valve) +Charizard +Mii +Rolls-Royce Limited +Copic +Mega Man Zero (video game) +Jak and Daxter +Priston Tale +Glacier +IPod Nano +Banknote +Mario & Sonic at the Olympic Games +Hero Factory +Bamboo +Fillet (cut) +Stencil +Winch +Dogfight +Treadmill +Bassoon +Staffordshire Bull Terrier +Cardboard +Epiphone Les Paul +Compact Cassette +Gelatin +White House +Suitcase +MX vs. ATV +Clank (Ratchet & Clank) +Beach volleyball +Loadout +Batter (cooking) +Zack Fair +Cliff +Baggage +Cream cheese +Lantern +Naruto: Clash of Ninja +Treasure +Raccoon +Mini 4WD +Robotic vacuum cleaner +Gate +Ribs (food) +Oatmeal +Water filter +Super Mario Sunshine +Animal Crossing: City Folk +Driver's license +Asus ZenFone +American black bear +Little Red Riding Hood +??? +Stable +Gashapon +Need for Speed: Underground +Dishwasher +Frying pan +Schutzhund +Mario Kart 7 +Disney Infinity +Saab Automobile +F-Zero +Halloween costume +Thor (Marvel Comics) +Foam +Tokyo Ghoul +Chevrolet Monte Carlo +Flush toilet +Axe +Worms (series) +Marble +Driver's education +Madden NFL 12 +Pressure washing +Christmas ornament +Buffalo wing +Duct (flow) +Indiana Jones +Chart +Yoshi's Island +Subaru Forester +Scar (The Lion King) +Mousse +Lalaloopsy +Micropterus +Gibson SG +Express train +Citroën C4 +Submission wrestling +Broccoli +Donkey Kong Country 2: Diddy's Kong Quest +Barrel organ +Mega Man 2 +Dragon boat +New Super Mario Bros. U +Gecko +Pillow +Kemenche +Porsche Cayenne +??? +Shift 2: Unleashed +Bomberman +Dungeons & Dragons +BeamNG.drive +AdventureQuest +Mario Kart 64 +Disc brake +Bloons Tower Defense +Forza Motorsport 3 +Guitar Center +Super Smash Bros. (video game) +Fiat Uno +Printed circuit board +Porcelain +E-book +Macaroni +Lego Friends +Max Payne 3 +StarCraft II: Heart of the Swarm +Medal of Honor: Warfighter +Kamaz +Air France +Porsche Carrera GT +Black Rock Shooter +Rosary +Halo Wars +Car dealership +Toys "R" Us +Total War: Rome II +Need for Speed: ProStreet +Mansion +Cheetah +Marshmallow +Shorts +Unturned +Charango +Lithium polymer battery +Sea turtle +Vatican City +Starbucks +Emergency vehicle lighting +Volkswagen Golf Mk1 +Lupin the Third +Pearl +Wii Sports +Hero +Chrysler 300 +GMC (automobile) +Charm bracelet +Kamen Rider Battle: Ganbaride +Ys (series) +Asus Eee Pad Transformer +BMW 5 Series (E60) +Ford Mustang SVT Cobra +Autocross +Royal icing +Laboratory +Peugeot 206 +Maltese (dog) +Soulcalibur IV +Wardrobe +Garlic +Tugboat +Luke Skywalker +Electronic circuit +Coat (clothing) +Passenger +??? +Cactus +Ford Crown Victoria +Elfen Lied +Circular saw +Radha +Welsh Corgi +Eiffel Tower +Softail +Bajo sexto +Lobster +Colt (horse) +Solar eclipse +Greyhound +Pepsi +Black Widow (Natasha Romanova) +Virtua Fighter +Filly +Canning +Fat +Goth subculture +Slow cooker +Lightning (Final Fantasy) +Water polo +Apple pie +Inkjet printing +Mercedes-Benz SLK-Class +Bandsaw +Cammy +Fight Night (EA video game series) +Tortoise +Multicooker +Ferret +Dipping sauce +Circle +Rocket launch +Pembroke Welsh Corgi +Cold porcelain +Battlefield Play4Free +ThinkPad +BMW X6 +??? +Sony Xperia Z +Selfie +Mahjong +Cherry +IPod Touch (5th generation) +Colin McRae: Dirt 2 +Tekken 5 +Shawl +Ultron +Guitar pick +Elk +Sunrise +Amusement arcade +Hammock +Decoupage +Mug +Sander +Autogyro +Woodchipper +Texas Instruments +Baby Alive +Tarantula +Shrub +Donkey Kong (video game) +Coating +Steirische Harmonika +Racing wheel +Raphael (Teenage Mutant Ninja Turtles) +Bank +Opel Vectra +Skull +Sand art and play +Birth +Lasagne +Infinity Ward +Philippine cuisine +Custard +Lettuce +Megami Tensei +Flappy Bird +Sleeping Dogs (video game) +Fender Jazz Bass +Devil Kings +Blouse +Notebook +Aloe vera +Funko +Lelouch Lamperouge +Macramé +Casserole +Capacitor +I Wanna Be the Guy +Hose +Subaru Legacy +Star Citizen +Sabian +Ventriloquism +Call of Duty (video game) +Kindle Fire +Starfire (Koriand'r) +Zeus +Microscope +Basket +Coyote +Bart Simpson +Volvo FH +Spinnerbait +Honda CR-V +Sony Xperia Z1 +Satan +Mercedes-Benz Sprinter +Team roping +Jeep Cherokee (XJ) +Friendship bracelet +Leonardo (Teenage Mutant Ninja Turtles) +Single track (mountain biking) +Chickpea +Vegetable carving +??? +Spark plug +Akita (dog) +Canoeing +Recumbent bicycle +Boom Beach +Puppetry +Sport stacking +Kendama +Punching bag +Staples Center +Marvel vs. Capcom 2: New Age of Heroes +Apple TV +Davul +Scratchcard +Disgaea +Larva +Used car +DmC: Devil May Cry +Kyo Kusanagi +Mega Man (video game) +K'Nex +Burger King +Dungeon crawl +Pro Evolution Soccer 2009 +Blueberry +Village +Convenience store +Golf cart +BMW M6 +Fiber +Resistance (series) +Picture frame +Trouble in Terrorist Town +Volkswagen Type 2 +Domestic pig +Grand Tourer Injection +Alucard (Hellsing) +Aerith Gainsborough +Batmobile +Gummi candy +Cauliflower +Marlin +Gold medal +Shin Megami Tensei: Persona 3 +Table football +Shikamaru Nara +Truggy +Ford Explorer +Chevrolet Cruze +American Airlines +Jupiter +Galaxy Nexus +KFC +Spec Ops: The Line +Rigs of Rods +EA Sports UFC +Plastic bottle +Hubble Space Telescope +Barn +Hand +Star Wars: Battlefront (2004 video game) +Digimon Masters +Gibson ES-335 +Waffle +Paper model +Ressha Sentai ToQger +Gas tungsten arc welding +Pavement (architecture) +Sonic & Sega All-Stars Racing +??? +Palace +Stealth game +God of War (2005 video game) +Mazda6 +Dragon Age II +Warhammer Online: Age of Reckoning +Switch +Grizzly bear +??? +H.A.V.E. Online +Lowlands (festival) +Wok +Window blind +Nokia N8 +Android Wear +V10 engine +Toyota Tundra +Marble (toy) +Alligator +Screencast +Range Rover Sport +Moose +Polo +Laminate flooring +BVE Trainsim +Baby sling +Garage door +Compact car +Dishonored +Parrot AR.Drone +Giraffe +Need for Speed Rivals +McLaren 12C +Pork ribs +Track cycling +Don't Starve +Marvel: Avengers Alliance +Popeye +Ford Mondeo +HTC One (M7) +Pyramid +Asphalt +Beetle +Canon EOS 600D +Oldsmobile Cutlass +Suzuki GSX-R750 +Audi A8 +World of Warcraft: The Burning Crusade +Homing pigeon +NHL 15 +Touring motorcycle +Goblin +Nissan 370Z +Metro: Last Light +Skylanders: Giants +Ran Online +Gear +Mercedes-Benz G-Class +Travian +Burnout Paradise +Tag team +Electric motorcycles and scooters +Kazuya Mishima +Serious Sam +Nexus 7 (2013) +Super Paper Mario +Doodle +Gelatin dessert +Andalusian horse +Warrior +Ferrari 360 +DVD player +WildStar (video game) +Hyundai Genesis +Chutney +Pizzica +Dead Rising 2 +Potter's wheel +Yoda +Cylinder (engine) +M. Bison +Metal Gear Solid: Peace Walker +Masonry +Edward Elric +Split (gymnastics) +Mario Kart DS +Ghost Rider +Grand Theft Auto: Episodes from Liberty City +F1 2012 (video game) +Cookie Monster +Red hair +Nami (One Piece) +Canon EF lens mount +Finger +Asteroid +Nissan Navara +Riddler +Traffic light +Nikon Coolpix series +Dragonica +Broth +Metal Gear Solid 2: Sons of Liberty +Samsung Galaxy Y +Wedding cake +Half-pipe +Gothic II +Vehicle horn +Motor oil +Credit card +Resident Evil 2 +British Airways +Great Dane +Stain +Super Mario 3D World +Yamaha YZ125 +Atari 2600 +Rover (space exploration) +Cayman +Ragdoll +Basement +Betta +Mobile home +Heroes of Might and Magic +Photograph +Wreath +Universe of The Legend of Zelda +Lamborghini Diablo +Albus Dumbledore +BlackBerry Bold +Prototype 2 +Soybean +Hurdling +Spock +Sony Xperia Z2 +Monopoly (game) +Fruit preserves +SimCity (2013 video game) +Cutlet +Volkswagen Touareg +Aerosol paint +Risotto +Toyota 4Runner +Driveclub +Moshing +Total War: Shogun 2 +Elf +Hot tub +President +NHL 13 +Rudolph the Red-Nosed Reindeer +Bugs Bunny +Mario & Luigi: Superstar Saga +Tulip +Paper Mario: The Thousand-Year Door +Hammer +EarthBound +Meta Knight +La Tale +Shadow of the Colossus +GLaDOS +Hunting dog +BioShock 2 +Supercars Championship +Orbit +God of War: Ascension +Bloons +Ney +Toyota MR2 +Cam +??? +Zoom lens +H&M +Hovercraft +Sanshin +Instant noodle +Luigi's Mansion +Tales of Vesperia +Dekotora +??? +Talking Tom and Friends +Baseball glove +Ale +Meringue +Canon EOS 7D +Shaolin Kung Fu +Hawk +Donkey Kong Country Returns +The Salvation Army +Brown trout +Sugarcane +Cake pop +Suzuki Bandit series +Green tea +Warehouse +Appalachian dulcimer +Kermit the Frog +Unicorn +Fountain pen +Acer Iconia +Master System +Robocraft +Merlin +Sweet potato +Alice's Adventures in Wonderland +Solar flare +DigiTech +Saturn +Flash (comics) +Reindeer +Justice League +Line Rider +Runes of Magic +Chevrolet Suburban +Michael Myers (Halloween) +Need for Speed: Undercover +Wand +Chevrolet Malibu +Coal +Antena 3 (Spain) +Driver: San Francisco +Font +Stingray +Thermostat +Toph Beifong +Vert ramp +Ridge Racer +Goat Simulator +Lineage (video game) +CNBC +Juri (Street Fighter) +TARDIS +Pigeon racing +Lap steel guitar +Shovel +Mosaic +Monster Retsuden Oreca Battle +Pair skating +Wallpaper +The Simpsons: Tapped Out +The Elder Scrolls III: Morrowind +Padel (sport) +Fender (vehicle) +Furnace +Nissan Altima +Cornet +Škoda Fabia +Lockheed Martin F-35 Lightning II +Electribe +Alesis +Motorola Razr +Halo: Combat Evolved Anniversary +Darksiders +Neo Geo (system) +Snail +Milking +Pluto (Disney) +Peanut +Verona Arena +Chubby Bunny +Jerry Mouse +Corvette Stingray (concept car) +Cigarette +Cube World +??? +Cybertron +Dacia Duster +Pastel +Transformer +Split screen (computer graphics) +Sukhoi Su-27 +Gabrielle (Xena: Warrior Princess) +Opel Kadett +Nokia Lumia 920 +Twin-turbo +Jiraiya (Naruto) +The Legend of Zelda: A Link to the Past +Crappie +Rechargeable battery +??? +Super Mario 3D Land +??? +DragonFable +Aragorn +Crash Bandicoot 2: Cortex Strikes Back +Southwest Airlines +Multi-tool +Passport +Porsche Panamera +Airship +Tuxedo Mask +Tom Clancy's Ghost Recon: Future Soldier +Melty Blood +Beam (structure) +Gas metal arc welding +Audi Q7 +Bell pepper +Chewing gum +Drinking water +Heat pump +Kenshiro +Patrick Drake and Robin Scorpio +Miniature wargaming +Kawasaki Ninja 650R +Captain Falcon +J-Stars Victory VS +Imperishable Night +Citrus +Drift trike +Optical illusion +Command & Conquer: Red Alert 3 +Suzuka Circuit +Mayonnaise +Quake III Arena +Keychain +God Mode +Ford Bronco +Crocodilia +Black and white +Llanero +Monorail +Nova +G.I. Joe +S.T.A.L.K.E.R.: Call of Pripyat +Perfect Cherry Blossom +Wine tasting +Olive +Ultra Series +Beat 'em up +Jellyfish +Lego Legends of Chima +Sauna +Tom Clancy's Splinter Cell: Blacklist +Starscream +Aang +Misty (Pokémon) +IPad Air +Ice pop +Lute +Jigsaw puzzle +Baritone saxophone +BMW Z4 +Mana (series) +Motorized bicycle +Dalmatian (dog) +Bose Corporation +Burton Snowboards +Kingdom Hearts: Chain of Memories +Mass Rapid Transit (Singapore) +Boombox +Napkin +Chimpanzee +Guitar Hero: Metallica +Radar detector +Honda NSX +Empire: Total War +Darts +Light fixture +Super Mario Bros. 2 +Temple Run +Kristoff (Disney) +Adrenalyn XL +Tatra (company) +Mini-Z +Tin can +Market garden +Mercedes-Benz Actros +Hug +Whipped cream +Wasp +Oni +Princess Daisy +Constellation +HTC One X +Fender Precision Bass +Prawn +Christmas card +Handbell +Coconut milk +Toshiba Satellite +Riven +Referee +Dragon's Dogma +Dalek +Folding bicycle +2 Days +Kimono +Seiko +Hippopotamus +Resident Evil: Revelations +Billboard (magazine) +Padlock +Butterfly stroke +Mashed potato +Yuan Zai (giant panda) +Aurora +Mop +Tubing (recreation) +Clothes iron +Order & Chaos Online +Zebra +Crème caramel +Warhammer 40,000: Dawn of War +Tom Clancy's Splinter Cell: Conviction +Wakfu +Stitch (Lilo & Stitch) +Calf +Cars 2 (video game) +Crayfish +Engagement ring +Infamous Second Son +Jukebox +Biryani +DJ Hero +Super GT +Chameleon +Oyster +Warcraft III: The Frozen Throne +Dynasty Warriors 7 +Postage stamp +Derek Shepherd +Plotter +Amnesia: The Dark Descent +Jinn +Rayman Legends +Tinker Bell +Patchwork +Doom 3 +Wat +Paiste +Mercedes-Benz CLS-Class +Liquid +GameTrailers +Pep squad +Clam +SaGa (series) +Nollie +Company of Heroes +Green Arrow +Naruto Uzumaki +DeWalt +Putter +Family +Transistor +SOCOM (series) +Pea +Social media +Aliens vs. Predator (2010 video game) +HTC HD2 +Ducati Monster +Aggressive inline skating +Maserati GranTurismo +PortAventura World +Lego Batman: The Videogame +Energy drink +Turban +Pokémon Yellow +Alaskan Malamute +Monica's Gang +Suzuki Vitara +Black Desert Online +Zara (retailer) +Just Dance 2015 +Maid Sama! +Disguise +Kidney +Water well +Farmer +Toyota RAV4 +Night +DJMax +Richter-tuned harmonica +Real Racing 3 +Solid Snake +United States dollar +F1 2010 (video game) +Samsung Galaxy Ace +Trials Evolution +Cadillac CTS +Daihatsu +Balcony +Xperia Play +Rookie +Timing belt (camshaft) +Monster Energy +Ork (Warhammer 40,000) +Toyota JZ engine +Drive-through +Spektrum RC +Hyundai Sonata +Chinchilla +Wii Sports Resort +Interchange (road) +Whitewater slalom +Ticket (admission) +Bayonetta +Salsa (sauce) +PlayStation All-Stars Battle Royale +Lego Minecraft +??? +Mule +Starbound +Scissors +Asparagus +Sony NEX-5 +Electrical connector +Rayquaza +Eight-ball +Steel-string acoustic guitar +Strap +Times Square +Bus driver +SEAT Ibiza +Converse (shoe company) +Atlantic bluefin tuna +Mercedes-Benz W124 +??? +Goggles +Kawasaki Z1000 +Shrimp and prawn as food +Garnier +Semi-trailer +Cod +Carpet cleaning +Lost Planet +Sonic the Hedgehog CD +Final Fantasy V +F1 2013 (video game) +Modelling clay +Audi Sportback concept +WWE All Stars +Mitsubishi Outlander +Punch-Out!! +Disney Infinity: Marvel Super Heroes +Mulch +Willy Wonka +Dead Space 3 +Eurofighter Typhoon +H1Z1: Just Survive +Fakie +Super Mario RPG +Dance Central 3 +Puppet +Cursor (user interface) +Prince of Persia: Warrior Within +Ultimate Mortal Kombat 3 +Macross +Upholstery +The Binding of Isaac (video game) +Deathstroke +The King of Fighters '98 +Dragon Ball Z: Battle of Z +Theatre organ +Valve Corporation +Age of Conan +GameStop +Unreal Tournament +Metroid Prime +Annie (musical) +Cinderella (Disney character) +Eric Cartman +The Prince of Tennis +Kia Sportage +Vase +Nightwing +Wing +Gouken +Loft +Ferris wheel +Newspaper +Cash +A Certain Magical Index +Pretty Rhythm +Marionette +Swing (seat) +He-Man +Cook (profession) +Bentley Continental GT +Shaman King +Hakuōki +Essential oil +Balalaika +Baja 1000 +Hummingbird +PSA HDi engine +Nissan Sentra +??? +Infamous (video game) +Game Boy Color +343 Industries +Six Flags Magic Mountain +Woozworld +It's a Small World +Star Fox 64 +Xenoblade Chronicles +TurboGrafx-16 +Tesla coil +HTC Evo 4G +Super Metroid +Label +Gothic (video game) +Samsung Galaxy Gear +??? +Viola caipira +Space Engineers +Yamaha MT-09 +Mortal Kombat: Armageddon +Angry Birds Star Wars +Aerography (arts) +Python (genus) +Hyundai Elantra +MG Cars +Tesla Model S +Castlevania: Symphony of the Night +Body armor +Bone +Tekken 5: Dark Resurrection +Kimchi +Wedding invitation +Porsche 930 +Whey protein +Winery +Honda Integra DC5 +Hatter (Alice's Adventures in Wonderland) +Double Dutch (jump rope) +Cort Guitars +One-man band +Dentures +Tupperware +The Lion King (musical) +BlackBerry Z10 +Kingdom Hearts III +Zipper +Leaf +Samsung Galaxy Note 10.1 +Bansuri +BMW 5 Series (F10) +Australian Shepherd +Crash Bandicoot: Warped +Pou (video game) +Tilapia +Peugeot 205 +AC Cobra +Tin whistle +Tooth brushing +Battlefield 1942 +Virginia Tech +Quarry +Amphibious ATV +Dome +Portable stove +Sound system (Jamaican) +Suikoden +Lunar eclipse +Tiramisu +Inazuma Eleven GO (video game) +Nissan 300ZX +Neverwinter (video game) +Axle +Altaïr Ibn-La'Ahad +Radiator +Resident Evil (2002 video game) +Prince of Persia: The Sands of Time +Crop circle +Rhinoceros +??? +Bookcase +Common quail +The Hunger Games +Mercedes-Benz A-Class +Sarah Walker (Chuck) +Cinnamon +Hiru TV +Bread roll +Magician (fantasy) +Lotion +Killzone 3 +Cadillac Escalade +Silhouette +Swan +Lemonade +Trabant +Mojito +Fossil +Macy's +Silk +Puma SE +Nissan Maxima +Battlefield 2142 +Twisted Metal +Olive oil +Wii Remote +Universal Studios Hollywood +Berserk (manga) +Wellington boot +Tomb Raider: Anniversary +Almond +Audi RS 6 +Ladder +Fire Emblem Awakening +Stained glass +Tape recorder +Emerald +Ford Fusion (Americas) +Iguana +Might and Magic +Pluto +Mazda Raceway Laguna Seca +Air Force 1 (shoe) +Pub +Oshun +Honda K engine +Nerd +Renault 5 +F1 2011 (video game) +Windscreen wiper +Lex Luthor +Track racing +Escalator +Charlie Brown +Chauffeur +Soba +Window film +Bowl +Alarm clock +Pokémon Mystery Dungeon: Explorers of Time and Explorers of Darkness +Roomba +Honda Shadow +Lightning Returns: Final Fantasy XIII +LATAM Brasil +Top +American Bulldog +Legoland +Caterpillar +Windows Phone 8 +Automated teller machine +Samsung Galaxy S III Mini +Portrait photography +Office +Para Para +Hockey stick +Singapore Airlines +Volvo S60 +Udon +Chevrolet K5 bazelr +Bath & Body Works +Segway PT +Castlevania: Lords of Shadow +Mario Kart: Double Dash +Mew (Pokémon) +Walkman +Mentos +Jilbāb +Canter and gallop +Cinderella +Skylanders: Trap Team +Lego Duplo +Morgan le Fay +Decal +Handycam +Women's Tennis Association +Yeti +Multi-valve +Pokémon Stadium +Matryoshka doll +Lexus LFA +Keirin +??? +Honda Prelude +Burrito +Midna +Shuriken +New Super Mario Bros. 2 +Nebula +BlackBerry PlayBook +Typography +Hare +Mohawk hairstyle +Onsen +Jet pack +Wagon +Just Dance 3 +Nissan S30 +Noah's Ark +Ronald McDonald +Bombardier Dash 8 +Raspberry +Hair dryer +The Simpsons: Hit & Run +Still life +Ice climbing +Lada Riva +Port +Compound bow +Resident Evil 3: Nemesis +R2-D2 +Sand animation +ABS-CBN (television network) +Leica Camera +Final Fantasy (video game) +Arkham Asylum +Dynasty Warriors 8 +Text messaging +Nursery (room) +Donkey Kong 64 +Star Wars Jedi Knight: Jedi Academy +Typing +Mapex Drums +Granado Espada +Calendar +UFC Undisputed 3 +Airbag +DMC World DJ Championships +Gingerbread +Rayman Origins +Lamborghini Reventón +Trials Fusion +Mafia (video game) +Paso Fino +??? +Sport kite +Taco Bell +Envelope +Mazdaspeed3 +Transformers: Generation 1 +Empanada +Mega Man 3 +Transformers: Fall of Cybertron +Rosalina (character) +Mosquito +Volkswagen Tiguan +Metal Gear Solid V: Ground Zeroes +Marmalade +Pandeiro +Miss Saigon +Yosemite National Park +Dutch Warmblood +Pre-flight safety demonstration +Citroën Saxo +Mack Trucks +Medley swimming +??? +Spindle (tool) +Greek cuisine +Hyundai Santa Fe +Chili con carne +Poster +Kawasaki Ninja 300 +Baby food +Grand Theft Auto (Game Boy Advance) +Sim racing +Chromebook +Peter Griffin +Stainless steel +Beverage can +Pixie cut +Chevrolet SS (concept car) +Chokehold +Bullion +Super Mario Kart +The Sims FreePlay +Giant Bicycles +Sgt. Frog +Age of Empires II +Abadá +Kingdom Hearts HD 1.5 Remix +Blackjack +Canon EOS 60D +Filling station +Plywood +Pheasant +Wilson Sporting Goods +Comb +Lighthouse +Rock and Roll Hall of Fame +Tōshirō Hitsugaya +Tales of the Abyss +Maze +Resident Evil: Operation Raccoon City +Cimbalom +??? +Monkey Island (series) +Civilization V +Venus +Peugeot 207 +The Amazing Spider-Man (2012 video game) +Chrono Cross +New Balance +Dassault Rafale +Daredevil (Marvel Comics character) +Silent Hill 2 +Beanie (seamed cap) +Nut (fruit) +Jill Valentine +Scion tC +Percy Jackson +Lord of the Dance (musical) +Far Cry (video game) +Star Wars: The Force Unleashed II +Memory card +Motorola Droid +Skylanders: Spyro's Adventure +Yamaha DT125 +Audi Q5 +Jaguar +Jaguar XJ +Animal Crossing: Wild World +Cockroach +Wetsuit +Funny Car +FarmVille +The Sims 3: Pets +Peel (fruit) +Melting +Aurora (Disney character) +Dry ice +Star Ocean +Duke Nukem Forever +Toribash +Yamaha YZ250 +Tekken 3 +Orihime Inoue +Spyro: Year of the Dragon +Eight-string guitar +Sonic Riders +Penny (The Big Bang Theory) +Honda XR series +Neodymium magnet toys +Leatherman +Maximum Destruction +Super Mario 64 DS +Unreal Tournament 3 +Health club +Chrysler Hemi engine +The North Face +CBS News +Pentium +Cannon +London Fashion Week +Military tactics +Smallmouth bass +Leopard gecko +Top (clothing) +Fable III +Panasonic Lumix DMC-GH4 +Sikorsky UH-60 Black Hawk +Blue Dragon +Loudspeaker enclosure +Ōkami +Tribal Wars +Hot chocolate +Beetroot +??? +Nokia N97 +Blue Exorcist +??? +Sonic and the Black Knight +Headscarf +Plasma display +Woody Woodpecker +??? +Beyblade: Shogun Steel +29er (bicycle) +QR code +Dyson (company) +Yanmar +Gladiator +Nissan Pathfinder +Nissan X-Trail +Autofocus +King Dedede +Zoo Tycoon 2 +Wheat tortilla +Team Rocket +Classical ballet +New York City Police Department +Heihachi Mishima +Crochet hook +Pencil case +Gods Eater Burst +??? +DS 3 +Periodic table +General Electric +Nissan Juke +Lollipop +Jaguar F-Type +MechWarrior Online +Dodge Neon SRT-4 +Fried egg +Revell +Indoor soccer +Gratin +Punisher +Washburn Guitars +Caster board +Eldar (Warhammer 40,000) +Final Fantasy Type-0 +NBA 2K10 +The Lord of the Rings: The Battle for Middle-earth II +Texas Longhorns +3D television +Scorpion +Warhammer 40,000: Dawn of War II +Burpee (exercise) +The Order: 1886 +Poptropica +Tomb Raider: Legend +Pelmeni +Bánh +PriPara +Legacy of Kain +Bowser Jr. +Yonex +Humanoid robot +Sony Ericsson Xperia X10 +Rain gutter +FIFA Street (2012 video game) +Castle Crashers +Meteoroid +Macaroni and cheese +Sega CD +Mac Mini +Tales of Xillia +Sonic Lost World +Orphanage +Siku Toys +Lego Batman 3: Beyond Gotham +Daenerys Targaryen +Orangutan +Town +Command & Conquer: Generals +Samurai Shodown +ZX Spectrum +Quake Live +Weighing scale +Dead Frontier +Wolfenstein: The New Order +Colin McRae: Dirt +Square dance +Assassin's Creed Rogue +Airboat +Uncharted: Drake's Fortune +Diddy Kong +Yamaha Motif +Theremin +Rilakkuma +Tie-dye +Flip-flops +Cylinder +Gothic 3 +Unreal (1998 video game) +Beyond: Two Souls +Umbrella +Dream Club +Gradius +Nexus One +Nokia N900 +Tamagotchi +Husband +Sleeping bag +Look-alike +Papaya +Mother 3 +The Beatles: Rock Band +Prince of Persia: The Two Thrones +??? +Darth Maul +Knife sharpening +Meteor shower +Flugelhorn +One Piece: Pirate Warriors +Asterix +Talk box +With Your Destiny +Alan Wake +Barcode +Recurve bow +Diaper bag +Ferrari F12berlinetta +Taskbar +Mortar (masonry) +Toner (skin care) +Freddy Krueger +Marriott International +Mass Effect (video game) +Hawkeye (comics) +Killing Floor (video game) +Chibiusa +Screenshot +Pear +Injury +Kia Sorento +Shredder (Teenage Mutant Ninja Turtles) +Lifeguard +Kei car +Fight Night Champion +Terra (comics) +Gamblerz diff --git a/mediapipe/graphs/youtube8m/local_video_model_inference.pbtxt b/mediapipe/graphs/youtube8m/local_video_model_inference.pbtxt new file mode 100644 index 000000000..3b598a534 --- /dev/null +++ b/mediapipe/graphs/youtube8m/local_video_model_inference.pbtxt @@ -0,0 +1,178 @@ +input_side_packet: "input_sequence_example_path" +input_side_packet: "input_video_path" +input_side_packet: "output_video_path" +input_side_packet: "segment_size" +input_side_packet: "overlap" + +node { + calculator: "LocalFileContentsCalculator" + input_side_packet: "FILE_PATH:input_sequence_example_path" + output_side_packet: "CONTENTS:input_sequence_example" +} + +node { + calculator: "StringToSequenceExampleCalculator" + input_side_packet: "STRING:input_sequence_example" + output_side_packet: "SEQUENCE_EXAMPLE:parsed_sequence_example" +} + +node { + calculator: "UnpackMediaSequenceCalculator" + input_side_packet: "SEQUENCE_EXAMPLE:parsed_sequence_example" + output_stream: "FLOAT_FEATURE_RGB:rgb_feature_vector" + output_stream: "FLOAT_FEATURE_AUDIO:audio_feature_vector" +} + +node { + calculator: "ConcatenateFloatVectorCalculator" + input_stream: "rgb_feature_vector" + input_stream: "audio_feature_vector" + output_stream: "feature_vector" +} + +node { + calculator: "VectorFloatToTensorCalculator" + input_stream: "feature_vector" + output_stream: "feature_tensor" +} + +node { + calculator: "StringToInt32Calculator" + input_side_packet: "segment_size" + output_side_packet: "segment_size_int" +} + +node { + calculator: "StringToInt32Calculator" + input_side_packet: "overlap" + output_side_packet: "overlap_int" +} + +node { + calculator: "LappedTensorBufferCalculator" + input_stream: "feature_tensor" + output_stream: "lapped_feature_tensor" + input_side_packet: "BUFFER_SIZE:segment_size_int" + input_side_packet: "OVERLAP:overlap_int" + node_options: { + [type.googleapis.com/mediapipe.LappedTensorBufferCalculatorOptions] { + add_batch_dim_to_tensors: true + } + } +} + +node { + calculator: "SidePacketToStreamCalculator" + input_side_packet: "segment_size_int" + output_stream: "AT_ZERO:segment_size_int_stream" +} + +node { + calculator: "VectorIntToTensorCalculator" + input_stream: "SINGLE_INT:segment_size_int_stream" + output_stream: "TENSOR_OUT:segment_size_tensor" +} + +node { + calculator: "PacketClonerCalculator" + input_stream: "segment_size_tensor" + input_stream: "lapped_feature_tensor" + output_stream: "synced_segment_size_tensor" +} + +node { + calculator: "TensorFlowSessionFromSavedModelCalculator" + output_side_packet: "SESSION:session" + node_options: { + [type.googleapis.com/mediapipe.TensorFlowSessionFromSavedModelCalculatorOptions]: { + saved_model_path: "/tmp/mediapipe/saved_model" + } + } +} + +node: { + calculator: "TensorFlowInferenceCalculator" + input_side_packet: "SESSION:session" + input_stream: "NUM_FRAMES:synced_segment_size_tensor" + input_stream: "RGB_AND_AUDIO:lapped_feature_tensor" + output_stream: "PREDICTIONS:prediction_tensor" + node_options: { + [type.googleapis.com/mediapipe.TensorFlowInferenceCalculatorOptions]: { + batch_size: 32 + } + } +} + +node { + calculator: "TensorToVectorFloatCalculator" + input_stream: "prediction_tensor" + output_stream: "prediction_vector" +} + +node { + calculator: "TopKScoresCalculator" + input_stream: "SCORES:prediction_vector" + output_stream: "TOP_K_INDEXES:top_k_indexes" + output_stream: "TOP_K_SCORES:top_k_scores" + output_stream: "TOP_K_LABELS:top_k_labels" + node_options: { + [type.googleapis.com/mediapipe.TopKScoresCalculatorOptions]: { + top_k: 3 + label_map_path: "mediapipe/graphs/youtube8m/label_map.txt" + } + } +} + +node { + calculator: "OpenCvVideoDecoderCalculator" + input_side_packet: "INPUT_FILE_PATH:input_video_path" + output_stream: "VIDEO:input_video" + output_stream: "VIDEO_PRESTREAM:input_video_header" +} + +node { + calculator: "LabelsToRenderDataCalculator" + input_stream: "LABELS:top_k_labels" + input_stream: "SCORES:top_k_scores" + input_stream: "VIDEO_PRESTREAM:input_video_header" + output_stream: "RENDER_DATA:render_data" + node_options: { + [type.googleapis.com/mediapipe.LabelsToRenderDataCalculatorOptions]: { + color { r: 255 g: 0 b: 0 } + color { r: 0 g: 255 b: 0 } + color { r: 0 g: 0 b: 255 } + thickness: 2.0 + font_height_px: 20 + max_num_labels: 3 + location: TOP_LEFT + } + } +} + +node { + calculator: "PacketClonerCalculator" + input_stream: "render_data" + input_stream: "input_video" + output_stream: "synchronized_render_data" +} + +node { + calculator: "AnnotationOverlayCalculator" + input_stream: "INPUT_FRAME:input_video" + input_stream: "synchronized_render_data" + output_stream: "OUTPUT_FRAME:output_video" +} + +node { + calculator: "OpenCvVideoEncoderCalculator" + input_stream: "VIDEO:output_video" + input_stream: "VIDEO_PRESTREAM:input_video_header" + input_side_packet: "OUTPUT_FILE_PATH:output_video_path" + node_options: { + [type.googleapis.com/mediapipe.OpenCvVideoEncoderCalculatorOptions]: { + codec: "avc1" + video_format: "mp4" + } + } +} + diff --git a/mediapipe/graphs/youtube8m/yt8m_dataset_model_inference.pbtxt b/mediapipe/graphs/youtube8m/yt8m_dataset_model_inference.pbtxt new file mode 100644 index 000000000..38a02570b --- /dev/null +++ b/mediapipe/graphs/youtube8m/yt8m_dataset_model_inference.pbtxt @@ -0,0 +1,139 @@ +input_side_packet: "desired_segment_size" +input_side_packet: "record_index" +input_side_packet: "tfrecord_path" +output_side_packet: "yt8m_id" +output_stream: "annotation_summary" + +node { + calculator: "StringToInt32Calculator" + input_side_packet: "record_index" + output_side_packet: "record_index_int" +} + +node { + calculator: "StringToInt32Calculator" + input_side_packet: "desired_segment_size" + output_side_packet: "desired_segment_size_int" +} + +node { + calculator: "TFRecordReaderCalculator" + input_side_packet: "TFRECORD_PATH:tfrecord_path" + input_side_packet: "RECORD_INDEX:record_index_int" + output_side_packet: "SEQUENCE_EXAMPLE:yt8m_sequence_example" +} + +node { + calculator: "UnpackYt8mSequenceExampleCalculator" + input_side_packet: "YT8M_SEQUENCE_EXAMPLE:yt8m_sequence_example" + input_side_packet: "DESIRED_SEGMENT_SIZE:desired_segment_size_int" + output_side_packet: "YT8M_ID:yt8m_id" + output_side_packet: "SEGMENT_SIZE:segment_size" + output_side_packet: "LAPPED_TENSOR_BUFFER_CALCULATOR_OPTIONS:lapped_tensor_buffer_calculator_options" + output_stream: "QUANTIZED_RGB_FEATURE:quantized_rgb_feature" + output_stream: "QUANTIZED_AUDIO_FEATURE:quantized_audio_feature" +} + +node { + calculator: "DequantizeByteArrayCalculator" + input_stream: "ENCODED:quantized_rgb_feature" + output_stream: "FLOAT_VECTOR:rgb_feature_vector" + node_options: { + [type.googleapis.com/mediapipe.DequantizeByteArrayCalculatorOptions]: { + max_quantized_value: 2 + min_quantized_value: -2 + } + } +} + +node { + calculator: "DequantizeByteArrayCalculator" + input_stream: "ENCODED:quantized_audio_feature" + output_stream: "FLOAT_VECTOR:audio_feature_vector" + node_options: { + [type.googleapis.com/mediapipe.DequantizeByteArrayCalculatorOptions]: { + max_quantized_value: 2 + min_quantized_value: -2 + } + } +} + +node { + calculator: "ConcatenateFloatVectorCalculator" + input_stream: "rgb_feature_vector" + input_stream: "audio_feature_vector" + output_stream: "feature_vector" +} + +node { + calculator: "VectorFloatToTensorCalculator" + input_stream: "feature_vector" + output_stream: "feature_tensor" +} + +node { + calculator: "LappedTensorBufferCalculator" + input_stream: "feature_tensor" + input_side_packet: "CALCULATOR_OPTIONS:lapped_tensor_buffer_calculator_options" + output_stream: "lapped_feature_tensor" +} + +node { + calculator: "SidePacketToStreamCalculator" + input_side_packet: "segment_size" + output_stream: "AT_ZERO:segment_size_int_stream" +} + +node { + calculator: "VectorIntToTensorCalculator" + input_stream: "SINGLE_INT:segment_size_int_stream" + output_stream: "TENSOR_OUT:segment_size_tensor" +} + +node { + calculator: "PacketClonerCalculator" + input_stream: "segment_size_tensor" + input_stream: "lapped_feature_tensor" + output_stream: "synced_segment_size_tensor" +} + +node { + calculator: "TensorFlowSessionFromSavedModelCalculator" + output_side_packet: "SESSION:session" + node_options: { + [type.googleapis.com/mediapipe.TensorFlowSessionFromSavedModelCalculatorOptions]: { + saved_model_path: "/tmp/mediapipe/saved_model" + } + } +} + +node: { + calculator: "TensorFlowInferenceCalculator" + input_side_packet: "SESSION:session" + input_stream: "NUM_FRAMES:synced_segment_size_tensor" + input_stream: "RGB_AND_AUDIO:lapped_feature_tensor" + output_stream: "PREDICTIONS:prediction_tensor" + node_options: { + [type.googleapis.com/mediapipe.TensorFlowInferenceCalculatorOptions]: { + batch_size: 32 + } + } +} + +node { + calculator: "TensorToVectorFloatCalculator" + input_stream: "prediction_tensor" + output_stream: "prediction_vector" +} + +node { + calculator: "TopKScoresCalculator" + input_stream: "SCORES:prediction_vector" + output_stream: "SUMMARY:annotation_summary" + node_options: { + [type.googleapis.com/mediapipe.TopKScoresCalculatorOptions]: { + top_k: 9 + label_map_path: "mediapipe/graphs/youtube8m/label_map.txt" + } + } +} diff --git a/mediapipe/java/com/google/mediapipe/BUILD b/mediapipe/java/com/google/mediapipe/BUILD new file mode 100644 index 000000000..82e2f52c2 --- /dev/null +++ b/mediapipe/java/com/google/mediapipe/BUILD @@ -0,0 +1,15 @@ +# Copyright 2019 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"]) # Apache 2.0 diff --git a/mediapipe/java/com/google/mediapipe/components/BUILD b/mediapipe/java/com/google/mediapipe/components/BUILD index 80b65e3d4..7fd808387 100644 --- a/mediapipe/java/com/google/mediapipe/components/BUILD +++ b/mediapipe/java/com/google/mediapipe/components/BUILD @@ -68,3 +68,10 @@ android_library( "@com_google_guava_android//jar", ], ) + +# Expose the java source files for building mediapipe AAR. +filegroup( + name = "java_src", + srcs = glob(["*.java"]), + visibility = ["//mediapipe:__subpackages__"], +) diff --git a/mediapipe/java/com/google/mediapipe/components/ExternalTextureConverter.java b/mediapipe/java/com/google/mediapipe/components/ExternalTextureConverter.java index 122f598ea..0d34e23e3 100644 --- a/mediapipe/java/com/google/mediapipe/components/ExternalTextureConverter.java +++ b/mediapipe/java/com/google/mediapipe/components/ExternalTextureConverter.java @@ -150,6 +150,7 @@ public class ExternalTextureConverter implements TextureFrameProducer { private ExternalTextureRenderer renderer = null; private long timestampOffset = 0; private long previousTimestamp = 0; + private boolean previousTimestampValid = false; protected int destinationWidth = 0; protected int destinationHeight = 0; @@ -335,11 +336,12 @@ public class ExternalTextureConverter implements TextureFrameProducer { // ensures that surface texture has the up-to-date timestamp. (Also adjust |timestampOffset| // to ensure that timestamps increase monotonically.) long textureTimestamp = surfaceTexture.getTimestamp() / NANOS_PER_MICRO; - if (textureTimestamp + timestampOffset <= previousTimestamp) { + if (previousTimestampValid && textureTimestamp + timestampOffset <= previousTimestamp) { timestampOffset = previousTimestamp + 1 - textureTimestamp; } outputFrame.setTimestamp(textureTimestamp + timestampOffset); previousTimestamp = outputFrame.getTimestamp(); + previousTimestampValid = true; } private void waitUntilReleased(AppTextureFrame frame) { diff --git a/mediapipe/java/com/google/mediapipe/framework/BUILD b/mediapipe/java/com/google/mediapipe/framework/BUILD index e6ad76ed9..5e582ebff 100644 --- a/mediapipe/java/com/google/mediapipe/framework/BUILD +++ b/mediapipe/java/com/google/mediapipe/framework/BUILD @@ -82,3 +82,10 @@ android_library( "@com_google_guava_android//jar", ], ) + +# Expose the java source files for building mediapipe AAR. +filegroup( + name = "java_src", + srcs = glob(["*.java"]), + visibility = ["//mediapipe:__subpackages__"], +) diff --git a/mediapipe/java/com/google/mediapipe/glutil/BUILD b/mediapipe/java/com/google/mediapipe/glutil/BUILD index fc378b4eb..4ad0d16d9 100644 --- a/mediapipe/java/com/google/mediapipe/glutil/BUILD +++ b/mediapipe/java/com/google/mediapipe/glutil/BUILD @@ -30,3 +30,10 @@ android_library( "@com_google_guava_android//jar", ], ) + +# Expose the java source files for building mediapipe AAR. +filegroup( + name = "java_src", + srcs = glob(["**/*.java"]), + visibility = ["//mediapipe:__subpackages__"], +) diff --git a/mediapipe/java/com/google/mediapipe/mediapipe_aar.bzl b/mediapipe/java/com/google/mediapipe/mediapipe_aar.bzl new file mode 100644 index 000000000..eaf4612cf --- /dev/null +++ b/mediapipe/java/com/google/mediapipe/mediapipe_aar.bzl @@ -0,0 +1,157 @@ +"""Generate MediaPipe AAR including different variants of .so in jni folder. + +Usage: + +Create a new mediapipe_aar() target in a BUILD file. For example, +putting the following code into mediapipe/examples/android/aar_demo/BUILD. + +``` +load("//mediapipe/java/com/google/mediapipe:mediapipe_aar.bzl", "mediapipe_aar") + +mediapipe_aar( + name = "my_aar", + calculators = ["//mediapipe/calculators/core:pass_through_calculator"], +) +``` + +Then, run the following Bazel command to generate the AAR. + +``` +$ bazel build -c opt --fat_apk_cpu=arm64-v8a,armeabi-v7a mediapipe/examples/android/aar_demo:my_aar +``` + +Finally, import the AAR into Android Studio. + +""" + +load("@build_bazel_rules_android//android:rules.bzl", "android_binary", "android_library") + +def mediapipe_aar(name, calculators = []): + """Generate MediaPipe AAR. + + Args: + name: the name of the AAR. + calculators: the calculator libraries to be compiled into the .so. + """ + native.cc_binary( + name = "libmediapipe_jni.so", + linkshared = 1, + linkstatic = 1, + deps = [ + "//mediapipe/java/com/google/mediapipe/framework/jni:mediapipe_framework_jni", + ] + calculators, + ) + + native.cc_library( + name = name + "_mediapipe_jni_lib", + srcs = [":libmediapipe_jni.so"], + alwayslink = 1, + ) + + native.genrule( + name = name + "_aar_manifest_generator", + outs = ["AndroidManifest.xml"], + cmd = """ +cat > $(OUTS) < + + + + +""", + ) + + native.genrule( + name = name + "_calculator_proto_java_src_generator", + srcs = [ + "//mediapipe/framework:protos_src", + "@com_google_protobuf_javalite//:well_known_protos", + ], + outs = ["CalculatorProto.java"], + cmd = "$(location @com_google_protobuf_javalite//:protoc) " + + "--plugin=protoc-gen-javalite=$(location @com_google_protobuf_javalite//:protoc_gen_javalite) " + + "--proto_path=. --proto_path=$(GENDIR) " + + "--proto_path=$$(pwd)/external/com_google_protobuf_javalite/src " + + "--javalite_out=$$(dirname $(location CalculatorProto.java)) mediapipe/framework/calculator.proto && " + + "mv $$(dirname $(location CalculatorProto.java))/com/google/mediapipe/proto/CalculatorProto.java $$(dirname $(location CalculatorProto.java))", + tools = [ + "@com_google_protobuf_javalite//:protoc", + "@com_google_protobuf_javalite//:protoc_gen_javalite", + ], + ) + + android_library( + name = name + "_android_lib", + srcs = [ + "//mediapipe/java/com/google/mediapipe/components:java_src", + "//mediapipe/java/com/google/mediapipe/framework:java_src", + "//mediapipe/java/com/google/mediapipe/glutil:java_src", + "CalculatorProto.java", + ], + manifest = "AndroidManifest.xml", + proguard_specs = ["//mediapipe/java/com/google/mediapipe/framework:proguard.pgcfg"], + deps = [ + ":" + name + "_mediapipe_jni_lib", + "//mediapipe/framework:calculator_java_proto_lite", + "//mediapipe/framework:calculator_profile_java_proto_lite", + "//mediapipe/framework/tool:calculator_graph_template_java_proto_lite", + "//third_party:androidx_annotation", + "//third_party:androidx_appcompat", + "//third_party:androidx_core", + "//third_party:androidx_legacy_support_v4", + "//third_party:camerax_core", + "//third_party:camera2", + "@com_google_code_findbugs//jar", + "@com_google_common_flogger//jar", + "@com_google_common_flogger_system_backend//jar", + "@com_google_guava_android//jar", + "@androidx_lifecycle//jar", + ], + ) + + _aar_with_jni(name, name + "_android_lib") + +def _aar_with_jni(name, android_library): + # Generate dummy AndroidManifest.xml for dummy apk usage + # (dummy apk is generated by _dummy_app target below) + native.genrule( + name = name + "_binary_manifest_generator", + outs = [name + "_generated_AndroidManifest.xml"], + cmd = """ +cat > $(OUTS) < + + +EOF +""", + ) + + # Generate dummy apk including .so files. + # We extract out .so files and throw away the apk. + android_binary( + name = name + "_dummy_app", + manifest = name + "_generated_AndroidManifest.xml", + custom_package = "dummy.package.for.so", + deps = [android_library], + ) + + native.genrule( + name = name, + srcs = [android_library + ".aar", name + "_dummy_app_unsigned.apk"], + outs = [name + ".aar"], + tags = ["manual"], + cmd = """ +cp $(location {}.aar) $(location :{}.aar) +chmod +w $(location :{}.aar) +origdir=$$PWD +cd $$(mktemp -d) +unzip $$origdir/$(location :{}_dummy_app_unsigned.apk) "lib/*" +cp -r lib jni +zip -r $$origdir/$(location :{}.aar) jni/*/*.so +""".format(android_library, name, name, name, name), + ) diff --git a/mediapipe/util/sequence/README.md b/mediapipe/util/sequence/README.md index 244ba82ed..18b795618 100644 --- a/mediapipe/util/sequence/README.md +++ b/mediapipe/util/sequence/README.md @@ -466,6 +466,7 @@ tasks and tracking (or class) fields for tracking information. |-----|------|------------------------|-------------| |`CLASS_SEGMENTATION/image/encoded`|feature list bytes|`add_class_segmentation_encoded` / `AddClassSegmentationEncoded`|The encoded image of class labels at each timestep.| |`CLASS_SEGMENTATION/image/timestamp`|feature list int|`add_class_segmentation_timestamp` / `AddClassSegmentationTimestamp`|The timestamp in microseconds for the class labels.| +|`CLASS_SEGMENTATION/image/multi_encoded`|feature list bytes list|`add_class_segmentation_multi_encoded` / `AddClassSegmentationMultiEncoded`|Storing multiple segmentation masks in case they overlap.| |`CLASS_SEGMENTATION/image/format`|context bytes|`set_class_segmentation_format` / `SetClassSegmentationFormat`|The encoding format of the class label images.| |`CLASS_SEGMENTATION/image/height`|context int|`set_class_segmentation_height` / `SetClassSegmentationHeight`|The height of the image in pixels.| |`CLASS_SEGMENTATION/image/width`|context int|`set_class_segmentation_width` / `SetClassSegmentationWidth`|The width of the image in pixels.| @@ -477,6 +478,7 @@ tasks and tracking (or class) fields for tracking information. |-----|------|------------------------|-------------| |`INSTANCE_SEGMENTATION/image/ encoded`|feature list bytes|`add_instance_segmentation_encoded` / `AddInstanceSegmentationEncoded`|The encoded image of object instance labels at each timestep.| |`INSTANCE_SEGMENTATION/image/ timestamp`|feature list int|`add_instance_segmentation_timestamp` / `AddInstanceSegmentationTimestamp`|The timestamp in microseconds for the object instance labels.| +|`INSTANCE_SEGMENTATION/image/multi_encoded`|feature list bytes list|`add_instance_segmentation_multi_encoded` / `AddInstanceSegmentationEncoded`|Storing multiple segmentation masks in case they overlap.| |`INSTANCE_SEGMENTATION/image/ format`|context bytes|`set_instance_segmentation_format` / `SetInstanceSegmentationFormat`|The encoding format of the object instance labels.| |`INSTANCE_SEGMENTATION/image/ height`|context int|`set_instance_segmentation_height` / `SetInstanceSegmentationHeight`|The height of the image in pixels.| |`INSTANCE_SEGMENTATION/image/ width`|context int|`set_instance_segmentation_width` / `SetInstanceSegmentationWidth`|The width of the image in pixels.| diff --git a/mediapipe/util/sequence/media_sequence.py b/mediapipe/util/sequence/media_sequence.py index 3191cffef..fc1f15d32 100644 --- a/mediapipe/util/sequence/media_sequence.py +++ b/mediapipe/util/sequence/media_sequence.py @@ -489,7 +489,9 @@ def _create_image_with_prefix(name, prefix): prefix=prefix, module_dict=globals()) msu.create_int_feature_list(name + "_timestamp", IMAGE_TIMESTAMP_KEY, prefix=prefix, module_dict=globals()) - + msu.create_bytes_list_feature_list(name + "_multi_encoded", + IMAGE_MULTI_ENCODED_KEY, prefix=prefix, + module_dict=globals()) FORWARD_FLOW_PREFIX = "FORWARD_FLOW" CLASS_SEGMENTATION_PREFIX = "CLASS_SEGMENTATION" INSTANCE_SEGMENTATION_PREFIX = "INSTANCE_SEGMENTATION" diff --git a/mediapipe/util/sequence/media_sequence_test.py b/mediapipe/util/sequence/media_sequence_test.py index 6c4846c4b..3a634c486 100644 --- a/mediapipe/util/sequence/media_sequence_test.py +++ b/mediapipe/util/sequence/media_sequence_test.py @@ -78,8 +78,10 @@ class MediaSequenceTest(tf.test.TestCase): ms.set_bbox_parts((b"HEAD", b"TOE"), example) # feature lists ms.add_image_encoded(b"test", example) + ms.add_image_multi_encoded([b"test", b"test"], example) ms.add_image_timestamp(47, example) ms.add_forward_flow_encoded(b"test", example) + ms.add_forward_flow_multi_encoded([b"test", b"test"], example) ms.add_forward_flow_timestamp(47, example) ms.add_bbox_ymin((0.47, 0.49), example) ms.add_bbox_xmin((0.47, 0.49), example) @@ -109,7 +111,9 @@ class MediaSequenceTest(tf.test.TestCase): ms.add_predicted_bbox_class_string((b"test", b"strings"), example) ms.add_predicted_bbox_timestamp(47, example) ms.add_class_segmentation_encoded(b"test", example) + ms.add_class_segmentation_multi_encoded([b"test", b"test"], example) ms.add_instance_segmentation_encoded(b"test", example) + ms.add_instance_segmentation_multi_encoded([b"test", b"test"], example) ms.add_class_segmentation_timestamp(47, example) ms.set_bbox_embedding_dimensions_per_region((47, 49), example) ms.set_bbox_embedding_format(b"test", example)