From a2a63e387680c090f76072864a107647e874d390 Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Mon, 30 Sep 2019 10:18:09 -0700 Subject: [PATCH] Project import generated by Copybara. GitOrigin-RevId: 796203faee20d7aae2876aac8ca5a1827dee4fe3 --- WORKSPACE | 6 + .../audio/audio_decoder_calculator.cc | 8 +- .../audio/stabilized_log_calculator.cc | 7 +- .../audio/stabilized_log_calculator_test.cc | 14 +- .../audio/time_series_framer_calculator.cc | 46 +- .../audio/time_series_framer_calculator.proto | 7 + .../time_series_framer_calculator_test.cc | 92 +- mediapipe/calculators/core/BUILD | 8 +- .../core/concatenate_vector_calculator.cc | 11 + .../core/concatenate_vector_calculator.h | 47 +- .../concatenate_vector_calculator_test.cc | 163 +++ mediapipe/calculators/image/BUILD | 50 +- .../image/bilateral_filter_calculator.cc | 63 +- .../image/image_cropping_calculator.cc | 157 ++- .../image/image_properties_calculator.cc | 12 +- .../image/image_transformation_calculator.cc | 51 +- .../calculators/image/recolor_calculator.cc | 58 +- .../calculators/image/set_alpha_calculator.cc | 64 +- mediapipe/calculators/tensorflow/BUILD | 4 +- .../pack_media_sequence_calculator.cc | 28 +- .../pack_media_sequence_calculator_test.cc | 142 +++ .../unpack_media_sequence_calculator.cc | 38 +- .../unpack_media_sequence_calculator.proto | 7 + .../unpack_media_sequence_calculator_test.cc | 57 + mediapipe/calculators/tflite/BUILD | 80 +- .../tflite/tflite_converter_calculator.cc | 199 ++-- .../tflite/tflite_converter_calculator.proto | 3 +- .../tflite/tflite_inference_calculator.cc | 242 ++-- ...tflite_tensors_to_detections_calculator.cc | 653 +++++++--- .../tflite_tensors_to_landmarks_calculator.cc | 10 +- ...lite_tensors_to_landmarks_calculator.proto | 6 + ...lite_tensors_to_segmentation_calculator.cc | 229 ++-- mediapipe/calculators/tflite/util.h | 25 + mediapipe/calculators/util/BUILD | 72 +- .../util/annotation_overlay_calculator.cc | 75 +- .../util/rect_to_render_data_calculator.cc | 78 +- .../util/top_k_scores_calculator.cc | 194 +++ .../util/top_k_scores_calculator.proto | 33 + .../util/top_k_scores_calculator_test.cc | 150 +++ mediapipe/docs/examples.md | 35 + mediapipe/docs/face_detection_desktop.md | 265 +++++ mediapipe/docs/face_detection_mobile_cpu.md | 244 ++++ mediapipe/docs/hair_segmentation_desktop.md | 209 ++++ mediapipe/docs/hand_detection_mobile_gpu.md | 4 +- mediapipe/docs/hand_tracking_desktop.md | 184 +++ mediapipe/docs/hand_tracking_mobile_gpu.md | 6 +- .../docs/images/face_detection_desktop.png | Bin 0 -> 121035 bytes .../docs/images/hand_tracking_desktop.png | Bin 0 -> 98586 bytes .../mobile/face_detection_mobile_cpu.png | Bin 0 -> 123891 bytes .../images/mobile/hand_tracking_mobile.png | Bin 49994 -> 53355 bytes mediapipe/docs/install.md | 26 +- mediapipe/docs/object_detection_desktop.md | 28 +- mediapipe/docs/visualizer.md | 2 +- mediapipe/examples/desktop/BUILD | 45 +- .../examples/desktop/demo_run_graph_main.cc | 146 +++ .../desktop/demo_run_graph_main_gpu.cc | 186 +++ .../examples/desktop/face_detection/BUILD | 34 + .../examples/desktop/hair_segmentation/BUILD | 26 + .../examples/desktop/hand_tracking/BUILD | 42 + .../examples/desktop/object_detection/BUILD | 8 + mediapipe/examples/desktop/youtube8m/BUILD | 2 +- .../examples/desktop/youtube8m/README.md | 4 +- .../generate_input_sequence_example.py | 15 +- mediapipe/framework/BUILD | 35 + .../framework/calculator_graph_bounds_test.cc | 777 +++++++++++- .../calculator_graph_side_packet_test.cc | 747 ++++++++++++ mediapipe/framework/calculator_graph_test.cc | 1047 +---------------- mediapipe/framework/formats/matrix_data.proto | 6 +- mediapipe/framework/graph_validation_test.cc | 12 +- mediapipe/framework/subgraph.cc | 4 +- mediapipe/framework/subgraph.h | 9 +- mediapipe/framework/test_calculators.cc | 42 +- mediapipe/framework/testdata/BUILD | 8 + .../framework/tool/subgraph_expansion.cc | 6 +- .../framework/tool/subgraph_expansion_test.cc | 4 +- mediapipe/gpu/BUILD | 23 +- mediapipe/gpu/MPPMetalUtil.h | 49 + mediapipe/gpu/MPPMetalUtil.mm | 51 + mediapipe/gpu/gl_calculator_helper_impl.h | 2 +- .../gpu/gl_calculator_helper_impl_common.cc | 27 +- .../gpu/gl_calculator_helper_impl_ios.mm | 4 +- mediapipe/gpu/gl_context_egl.cc | 30 +- mediapipe/gpu/gl_texture_buffer.cc | 3 +- mediapipe/gpu/gpu_buffer_test.cc | 50 + mediapipe/gpu/gpu_shared_data_internal.h | 2 +- mediapipe/gpu/gpu_test_base.h | 39 + mediapipe/graphs/face_detection/BUILD | 17 + .../face_detection_desktop_live.pbtxt | 184 +++ .../face_detection_mobile_cpu.pbtxt | 8 +- .../face_detection_mobile_gpu.pbtxt | 10 +- .../hair_segmentation_mobile_gpu.pbtxt | 2 +- mediapipe/graphs/hand_tracking/BUILD | 74 +- .../hand_detection_desktop.pbtxt | 62 + .../hand_detection_desktop_live.pbtxt | 38 + .../hand_tracking/hand_tracking_desktop.pbtxt | 126 ++ .../hand_tracking_desktop_live.pbtxt | 103 ++ .../graphs/hand_tracking/subgraphs/BUILD | 132 +++ .../subgraphs/hand_detection_cpu.pbtxt | 193 +++ .../{ => subgraphs}/hand_detection_gpu.pbtxt | 8 +- .../subgraphs/hand_landmark_cpu.pbtxt | 185 +++ .../{ => subgraphs}/hand_landmark_gpu.pbtxt | 7 +- .../subgraphs/renderer_cpu.pbtxt | 102 ++ .../{ => subgraphs}/renderer_gpu.pbtxt | 0 mediapipe/graphs/object_detection/BUILD | 4 + .../object_detection_desktop_live.pbtxt | 174 +++ .../object_detection_mobile_cpu.pbtxt | 4 +- .../object_detection_mobile_gpu.pbtxt | 8 +- mediapipe/graphs/youtube8m/BUILD | 2 +- .../graphs/youtube8m/feature_extraction.pbtxt | 10 +- .../mediapipe/components/FrameProcessor.java | 10 +- .../com/google/mediapipe/framework/Graph.java | 3 +- .../com/google/mediapipe/framework/jni/BUILD | 7 +- .../google/mediapipe/framework/jni/graph.h | 2 +- mediapipe/objc/MPPGraph.h | 7 + mediapipe/objc/MPPGraph.mm | 41 +- mediapipe/objc/MPPGraphTestBase.h | 3 + mediapipe/objc/MPPGraphTestBase.mm | 8 +- mediapipe/util/android/file/base/helpers.cc | 10 +- mediapipe/util/annotation_renderer.cc | 3 +- mediapipe/util/resource_util_android.cc | 27 +- mediapipe/util/resource_util_apple.cc | 37 +- ...9e5ea6ef59562b030248947f787d1256132ae.diff | 58 + 122 files changed, 7330 insertions(+), 2016 deletions(-) create mode 100644 mediapipe/calculators/tflite/util.h create mode 100644 mediapipe/calculators/util/top_k_scores_calculator.cc create mode 100644 mediapipe/calculators/util/top_k_scores_calculator.proto create mode 100644 mediapipe/calculators/util/top_k_scores_calculator_test.cc create mode 100644 mediapipe/docs/face_detection_desktop.md create mode 100644 mediapipe/docs/face_detection_mobile_cpu.md create mode 100644 mediapipe/docs/hair_segmentation_desktop.md create mode 100644 mediapipe/docs/hand_tracking_desktop.md create mode 100644 mediapipe/docs/images/face_detection_desktop.png create mode 100644 mediapipe/docs/images/hand_tracking_desktop.png create mode 100644 mediapipe/docs/images/mobile/face_detection_mobile_cpu.png create mode 100644 mediapipe/examples/desktop/demo_run_graph_main.cc create mode 100644 mediapipe/examples/desktop/demo_run_graph_main_gpu.cc create mode 100644 mediapipe/examples/desktop/face_detection/BUILD create mode 100644 mediapipe/examples/desktop/hair_segmentation/BUILD create mode 100644 mediapipe/examples/desktop/hand_tracking/BUILD create mode 100644 mediapipe/framework/calculator_graph_side_packet_test.cc create mode 100644 mediapipe/gpu/MPPMetalUtil.h create mode 100644 mediapipe/gpu/MPPMetalUtil.mm create mode 100644 mediapipe/gpu/gpu_buffer_test.cc create mode 100644 mediapipe/gpu/gpu_test_base.h create mode 100644 mediapipe/graphs/face_detection/face_detection_desktop_live.pbtxt create mode 100644 mediapipe/graphs/hand_tracking/hand_detection_desktop.pbtxt create mode 100644 mediapipe/graphs/hand_tracking/hand_detection_desktop_live.pbtxt create mode 100644 mediapipe/graphs/hand_tracking/hand_tracking_desktop.pbtxt create mode 100644 mediapipe/graphs/hand_tracking/hand_tracking_desktop_live.pbtxt create mode 100644 mediapipe/graphs/hand_tracking/subgraphs/BUILD create mode 100644 mediapipe/graphs/hand_tracking/subgraphs/hand_detection_cpu.pbtxt rename mediapipe/graphs/hand_tracking/{ => subgraphs}/hand_detection_gpu.pbtxt (96%) create mode 100644 mediapipe/graphs/hand_tracking/subgraphs/hand_landmark_cpu.pbtxt rename mediapipe/graphs/hand_tracking/{ => subgraphs}/hand_landmark_gpu.pbtxt (96%) create mode 100644 mediapipe/graphs/hand_tracking/subgraphs/renderer_cpu.pbtxt rename mediapipe/graphs/hand_tracking/{ => subgraphs}/renderer_gpu.pbtxt (100%) create mode 100644 mediapipe/graphs/object_detection/object_detection_desktop_live.pbtxt create mode 100644 third_party/com_github_glog_glog_9779e5ea6ef59562b030248947f787d1256132ae.diff diff --git a/WORKSPACE b/WORKSPACE index 31a7a1b29..0aee35c67 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -64,6 +64,12 @@ http_archive( sha256 = "267103f8a1e9578978aa1dc256001e6529ef593e5aea38193d31c2872ee025e8", strip_prefix = "glog-0.3.5", build_file = "@//third_party:glog.BUILD", + patches = [ + "@//third_party:com_github_glog_glog_9779e5ea6ef59562b030248947f787d1256132ae.diff" + ], + patch_args = [ + "-p1", + ], ) # libyuv diff --git a/mediapipe/calculators/audio/audio_decoder_calculator.cc b/mediapipe/calculators/audio/audio_decoder_calculator.cc index 24689492e..b80b64bae 100644 --- a/mediapipe/calculators/audio/audio_decoder_calculator.cc +++ b/mediapipe/calculators/audio/audio_decoder_calculator.cc @@ -61,7 +61,9 @@ class AudioDecoderCalculator : public CalculatorBase { ::mediapipe::Status AudioDecoderCalculator::GetContract( CalculatorContract* cc) { cc->InputSidePackets().Tag("INPUT_FILE_PATH").Set(); - + if (cc->InputSidePackets().HasTag("OPTIONS")) { + cc->InputSidePackets().Tag("OPTIONS").Set(); + } cc->Outputs().Tag("AUDIO").Set(); if (cc->Outputs().HasTag("AUDIO_HEADER")) { cc->Outputs().Tag("AUDIO_HEADER").SetNone(); @@ -72,7 +74,9 @@ class AudioDecoderCalculator : public CalculatorBase { ::mediapipe::Status AudioDecoderCalculator::Open(CalculatorContext* cc) { const std::string& input_file_path = cc->InputSidePackets().Tag("INPUT_FILE_PATH").Get(); - const auto& decoder_options = cc->Options(); + const auto& decoder_options = + tool::RetrieveOptions(cc->Options(), + cc->InputSidePackets(), "OPTIONS"); decoder_ = absl::make_unique(); MP_RETURN_IF_ERROR(decoder_->Initialize(input_file_path, decoder_options)); std::unique_ptr header = diff --git a/mediapipe/calculators/audio/stabilized_log_calculator.cc b/mediapipe/calculators/audio/stabilized_log_calculator.cc index 50ccc01a0..b5623ee0f 100644 --- a/mediapipe/calculators/audio/stabilized_log_calculator.cc +++ b/mediapipe/calculators/audio/stabilized_log_calculator.cc @@ -75,8 +75,13 @@ class StabilizedLogCalculator : public CalculatorBase { ::mediapipe::Status Process(CalculatorContext* cc) override { auto input_matrix = cc->Inputs().Index(0).Get(); + if (input_matrix.array().isNaN().any()) { + return ::mediapipe::InvalidArgumentError("NaN input to log operation."); + } if (check_nonnegativity_) { - CHECK_GE(input_matrix.minCoeff(), 0); + if (input_matrix.minCoeff() < 0.0) { + return ::mediapipe::OutOfRangeError("Negative input to log operation."); + } } std::unique_ptr output_frame(new Matrix( output_scale_ * (input_matrix.array() + stabilizer_).log().matrix())); diff --git a/mediapipe/calculators/audio/stabilized_log_calculator_test.cc b/mediapipe/calculators/audio/stabilized_log_calculator_test.cc index 9831f4fe9..e6e0b5c6f 100644 --- a/mediapipe/calculators/audio/stabilized_log_calculator_test.cc +++ b/mediapipe/calculators/audio/stabilized_log_calculator_test.cc @@ -11,6 +11,7 @@ // 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 "Eigen/Core" #include "mediapipe/calculators/audio/stabilized_log_calculator.pb.h" @@ -108,13 +109,22 @@ TEST_F(StabilizedLogCalculatorTest, ZerosAreStabilized) { runner_->Outputs().Index(0).packets[0].Get()); } -TEST_F(StabilizedLogCalculatorTest, NegativeValuesCheckFail) { +TEST_F(StabilizedLogCalculatorTest, NanValuesReturnError) { + InitializeGraph(); + FillInputHeader(); + AppendInputPacket( + new Matrix(Matrix::Constant(kNumChannels, kNumSamples, std::nanf(""))), + 0 /* timestamp */); + ASSERT_FALSE(RunGraph().ok()); +} + +TEST_F(StabilizedLogCalculatorTest, NegativeValuesReturnError) { InitializeGraph(); FillInputHeader(); AppendInputPacket( new Matrix(Matrix::Constant(kNumChannels, kNumSamples, -1.0)), 0 /* timestamp */); - ASSERT_DEATH(RunGraphNoReturn(), ""); + ASSERT_FALSE(RunGraph().ok()); } TEST_F(StabilizedLogCalculatorTest, NegativeValuesDoNotCheckFailIfCheckIsOff) { diff --git a/mediapipe/calculators/audio/time_series_framer_calculator.cc b/mediapipe/calculators/audio/time_series_framer_calculator.cc index 34adb5700..04f593bca 100644 --- a/mediapipe/calculators/audio/time_series_framer_calculator.cc +++ b/mediapipe/calculators/audio/time_series_framer_calculator.cc @@ -56,6 +56,14 @@ namespace mediapipe { // If pad_final_packet is true, all input samples will be emitted and the final // packet will be zero padded as necessary. If pad_final_packet is false, some // samples may be dropped at the end of the stream. +// +// If use_local_timestamp is true, the output packet's timestamp is based on the +// last sample of the packet. The timestamp of this sample is inferred by +// input_packet_timesamp + local_sample_index / sampling_rate_. If false, the +// output packet's timestamp is based on the cumulative timestamping, which is +// done by adopting the timestamp of the first sample of the packet and this +// sample's timestamp is inferred by initial_input_timestamp_ + +// cumulative_completed_samples / sample_rate_. class TimeSeriesFramerCalculator : public CalculatorBase { public: static ::mediapipe::Status GetContract(CalculatorContract* cc) { @@ -86,11 +94,26 @@ class TimeSeriesFramerCalculator : public CalculatorBase { void FrameOutput(CalculatorContext* cc); Timestamp CurrentOutputTimestamp() { + if (use_local_timestamp_) { + return current_timestamp_; + } + return CumulativeOutputTimestamp(); + } + + Timestamp CumulativeOutputTimestamp() { return initial_input_timestamp_ + round(cumulative_completed_samples_ / sample_rate_ * Timestamp::kTimestampUnitsPerSecond); } + // Returns the timestamp of a sample on a base, which is usually the time + // stamp of a packet. + Timestamp CurrentSampleTimestamp(const Timestamp& timestamp_base, + int64 number_of_samples) { + return timestamp_base + round(number_of_samples / sample_rate_ * + Timestamp::kTimestampUnitsPerSecond); + } + // The number of input samples to advance after the current output frame is // emitted. int next_frame_step_samples() const { @@ -118,14 +141,18 @@ class TimeSeriesFramerCalculator : public CalculatorBase { // any overlap). int64 cumulative_completed_samples_; Timestamp initial_input_timestamp_; + // The current timestamp is updated along with the incoming packets. + Timestamp current_timestamp_; int num_channels_; // Each entry in this deque consists of a single sample, i.e. a - // single column vector. - std::deque sample_buffer_; + // single column vector, and its timestamp. + std::deque> sample_buffer_; bool use_window_; Matrix window_; + + bool use_local_timestamp_; }; REGISTER_CALCULATOR(TimeSeriesFramerCalculator); @@ -133,7 +160,8 @@ void TimeSeriesFramerCalculator::EnqueueInput(CalculatorContext* cc) { const Matrix& input_frame = cc->Inputs().Index(0).Get(); for (int i = 0; i < input_frame.cols(); ++i) { - sample_buffer_.emplace_back(input_frame.col(i)); + sample_buffer_.emplace_back(std::make_pair( + input_frame.col(i), CurrentSampleTimestamp(cc->InputTimestamp(), i))); } cumulative_input_samples_ += input_frame.cols(); @@ -151,14 +179,16 @@ void TimeSeriesFramerCalculator::FrameOutput(CalculatorContext* cc) { new Matrix(num_channels_, frame_duration_samples_)); for (int i = 0; i < std::min(frame_step_samples, frame_duration_samples_); ++i) { - output_frame->col(i) = sample_buffer_.front(); + output_frame->col(i) = sample_buffer_.front().first; + current_timestamp_ = sample_buffer_.front().second; sample_buffer_.pop_front(); } const int frame_overlap_samples = frame_duration_samples_ - frame_step_samples; if (frame_overlap_samples > 0) { for (int i = 0; i < frame_overlap_samples; ++i) { - output_frame->col(i + frame_step_samples) = sample_buffer_[i]; + output_frame->col(i + frame_step_samples) = sample_buffer_[i].first; + current_timestamp_ = sample_buffer_[i].second; } } else { samples_still_to_drop_ = -frame_overlap_samples; @@ -178,6 +208,7 @@ void TimeSeriesFramerCalculator::FrameOutput(CalculatorContext* cc) { ::mediapipe::Status TimeSeriesFramerCalculator::Process(CalculatorContext* cc) { if (initial_input_timestamp_ == Timestamp::Unstarted()) { initial_input_timestamp_ = cc->InputTimestamp(); + current_timestamp_ = initial_input_timestamp_; } EnqueueInput(cc); @@ -195,7 +226,8 @@ void TimeSeriesFramerCalculator::FrameOutput(CalculatorContext* cc) { std::unique_ptr output_frame(new Matrix); output_frame->setZero(num_channels_, frame_duration_samples_); for (int i = 0; i < sample_buffer_.size(); ++i) { - output_frame->col(i) = sample_buffer_[i]; + output_frame->col(i) = sample_buffer_[i].first; + current_timestamp_ = sample_buffer_[i].second; } cc->Outputs().Index(0).Add(output_frame.release(), @@ -258,6 +290,7 @@ void TimeSeriesFramerCalculator::FrameOutput(CalculatorContext* cc) { cumulative_output_frames_ = 0; samples_still_to_drop_ = 0; initial_input_timestamp_ = Timestamp::Unstarted(); + current_timestamp_ = Timestamp::Unstarted(); std::vector window_vector; use_window_ = false; @@ -282,6 +315,7 @@ void TimeSeriesFramerCalculator::FrameOutput(CalculatorContext* cc) { frame_duration_samples_) .cast(); } + use_local_timestamp_ = framer_options.use_local_timestamp(); return ::mediapipe::OkStatus(); } diff --git a/mediapipe/calculators/audio/time_series_framer_calculator.proto b/mediapipe/calculators/audio/time_series_framer_calculator.proto index 61be38da7..9e5b07462 100644 --- a/mediapipe/calculators/audio/time_series_framer_calculator.proto +++ b/mediapipe/calculators/audio/time_series_framer_calculator.proto @@ -62,4 +62,11 @@ message TimeSeriesFramerCalculatorOptions { HANN = 2; } optional WindowFunction window_function = 4 [default = NONE]; + + // If use_local_timestamp is true, the output packet's timestamp is based on + // the last sample of the packet and it's inferred from the latest input + // packet's timestamp. If false, the output packet's timestamp is based on + // the cumulative timestamping, which is inferred from the intial input + // timestamp and the cumulative number of samples. + optional bool use_local_timestamp = 6 [default = false]; } diff --git a/mediapipe/calculators/audio/time_series_framer_calculator_test.cc b/mediapipe/calculators/audio/time_series_framer_calculator_test.cc index 1a370faa1..cd0c38e13 100644 --- a/mediapipe/calculators/audio/time_series_framer_calculator_test.cc +++ b/mediapipe/calculators/audio/time_series_framer_calculator_test.cc @@ -35,6 +35,8 @@ namespace mediapipe { namespace { const int kInitialTimestampOffsetMicroseconds = 4; +const int kGapBetweenPacketsInSeconds = 1; +const int kUniversalInputPacketSize = 50; class TimeSeriesFramerCalculatorTest : public TimeSeriesCalculatorTest { @@ -391,5 +393,93 @@ TEST_F(TimeSeriesFramerCalculatorWindowingSanityTest, HannWindowSanityCheck) { RunAndTestSinglePacketAverage(0.5f); } -} // anonymous namespace +// A simple test class that checks the local packet time stamp. This class +// generate a series of packets with and without gaps between packets and tests +// the behavior with cumulative timestamping and local packet timestamping. +class TimeSeriesFramerCalculatorTimestampingTest + : public TimeSeriesFramerCalculatorTest { + protected: + // Creates test input and saves a reference copy. + void InitializeInputForTimeStampingTest() { + concatenated_input_samples_.resize(0, num_input_channels_); + num_input_samples_ = 0; + for (int i = 0; i < 10; ++i) { + // This range of packet sizes was chosen such that some input + // packets will be smaller than the output packet size and other + // input packets will be larger. + int packet_size = kUniversalInputPacketSize; + double timestamp_seconds = kInitialTimestampOffsetMicroseconds * 1.0e-6 + + num_input_samples_ / input_sample_rate_; + if (options_.use_local_timestamp()) { + timestamp_seconds += kGapBetweenPacketsInSeconds * i; + } + + Matrix* data_frame = + NewTestFrame(num_input_channels_, packet_size, timestamp_seconds); + + AppendInputPacket(data_frame, round(timestamp_seconds * + Timestamp::kTimestampUnitsPerSecond)); + num_input_samples_ += packet_size; + } + } + + void CheckOutputTimestamps() { + int num_full_packets = output().packets.size(); + if (options_.pad_final_packet()) { + num_full_packets -= 1; + } + + int64 num_samples = 0; + for (int packet_num = 0; packet_num < num_full_packets; ++packet_num) { + const Packet& packet = output().packets[packet_num]; + num_samples += FrameDurationSamples(); + double expected_timestamp = + options_.use_local_timestamp() + ? GetExpectedLocalTimestampForSample(num_samples - 1) + : GetExpectedCumulativeTimestamp(num_samples - 1); + ASSERT_NEAR(packet.Timestamp().Seconds(), expected_timestamp, 1e-10); + } + } + + ::mediapipe::Status RunTimestampTest() { + InitializeGraph(); + InitializeInputForTimeStampingTest(); + FillInputHeader(); + return RunGraph(); + } + + private: + // Returns the timestamp in seconds based on local timestamping. + double GetExpectedLocalTimestampForSample(int sample_index) { + return kInitialTimestampOffsetMicroseconds * 1.0e-6 + + sample_index / input_sample_rate_ + + (sample_index / kUniversalInputPacketSize) * + kGapBetweenPacketsInSeconds; + } + + // Returns the timestamp inseconds based on cumulative timestamping. + double GetExpectedCumulativeTimestamp(int sample_index) { + return kInitialTimestampOffsetMicroseconds * 1.0e-6 + + sample_index / FrameDurationSamples() * FrameDurationSamples() / + input_sample_rate_; + } +}; + +TEST_F(TimeSeriesFramerCalculatorTimestampingTest, UseLocalTimeStamp) { + options_.set_frame_duration_seconds(100.0 / input_sample_rate_); + options_.set_use_local_timestamp(true); + + MP_ASSERT_OK(RunTimestampTest()); + CheckOutputTimestamps(); +} + +TEST_F(TimeSeriesFramerCalculatorTimestampingTest, UseCumulativeTimeStamp) { + options_.set_frame_duration_seconds(100.0 / input_sample_rate_); + options_.set_use_local_timestamp(false); + + MP_ASSERT_OK(RunTimestampTest()); + CheckOutputTimestamps(); +} + +} // namespace } // namespace mediapipe diff --git a/mediapipe/calculators/core/BUILD b/mediapipe/calculators/core/BUILD index ebef8127f..80205f90e 100644 --- a/mediapipe/calculators/core/BUILD +++ b/mediapipe/calculators/core/BUILD @@ -166,7 +166,13 @@ cc_library( "//mediapipe/framework/port:ret_check", "//mediapipe/framework/port:status", "@org_tensorflow//tensorflow/lite:framework", - ], + ] + select({ + "//mediapipe/gpu:disable_gpu": [], + "//mediapipe:ios": [], + "//conditions:default": [ + "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_buffer", + ], + }), alwayslink = 1, ) diff --git a/mediapipe/calculators/core/concatenate_vector_calculator.cc b/mediapipe/calculators/core/concatenate_vector_calculator.cc index 7a8445f48..c4144990e 100644 --- a/mediapipe/calculators/core/concatenate_vector_calculator.cc +++ b/mediapipe/calculators/core/concatenate_vector_calculator.cc @@ -19,6 +19,10 @@ #include "mediapipe/framework/formats/landmark.pb.h" #include "tensorflow/lite/interpreter.h" +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +#include "tensorflow/lite/delegates/gpu/gl/gl_buffer.h" +#endif // !MEDIAPIPE_DISABLE_GPU + namespace mediapipe { // Example config: @@ -45,4 +49,11 @@ REGISTER_CALCULATOR(ConcatenateTfLiteTensorVectorCalculator); typedef ConcatenateVectorCalculator<::mediapipe::NormalizedLandmark> ConcatenateLandmarkVectorCalculator; REGISTER_CALCULATOR(ConcatenateLandmarkVectorCalculator); + +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +typedef ConcatenateVectorCalculator<::tflite::gpu::gl::GlBuffer> + ConcatenateGlBufferVectorCalculator; +REGISTER_CALCULATOR(ConcatenateGlBufferVectorCalculator); +#endif + } // namespace mediapipe diff --git a/mediapipe/calculators/core/concatenate_vector_calculator.h b/mediapipe/calculators/core/concatenate_vector_calculator.h index b7ee24a9c..08e8e954f 100644 --- a/mediapipe/calculators/core/concatenate_vector_calculator.h +++ b/mediapipe/calculators/core/concatenate_vector_calculator.h @@ -15,6 +15,7 @@ #ifndef MEDIAPIPE_CALCULATORS_CORE_CONCATENATE_VECTOR_CALCULATOR_H_ #define MEDIAPIPE_CALCULATORS_CORE_CONCATENATE_VECTOR_CALCULATOR_H_ +#include #include #include "mediapipe/calculators/core/concatenate_vector_calculator.pb.h" @@ -59,16 +60,58 @@ class ConcatenateVectorCalculator : public CalculatorBase { if (cc->Inputs().Index(i).IsEmpty()) return ::mediapipe::OkStatus(); } } - auto output = absl::make_unique>(); + + return ConcatenateVectors(std::is_copy_constructible(), cc); + } + + template + ::mediapipe::Status ConcatenateVectors(std::true_type, + CalculatorContext* cc) { + auto output = absl::make_unique>(); for (int i = 0; i < cc->Inputs().NumEntries(); ++i) { if (cc->Inputs().Index(i).IsEmpty()) continue; - const std::vector& input = cc->Inputs().Index(i).Get>(); + const std::vector& input = cc->Inputs().Index(i).Get>(); output->insert(output->end(), input.begin(), input.end()); } cc->Outputs().Index(0).Add(output.release(), cc->InputTimestamp()); return ::mediapipe::OkStatus(); } + template + ::mediapipe::Status ConcatenateVectors(std::false_type, + CalculatorContext* cc) { + return ConsumeAndConcatenateVectors(std::is_move_constructible(), cc); + } + + template + ::mediapipe::Status ConsumeAndConcatenateVectors(std::true_type, + CalculatorContext* cc) { + auto output = absl::make_unique>(); + for (int i = 0; i < cc->Inputs().NumEntries(); ++i) { + if (cc->Inputs().Index(i).IsEmpty()) continue; + ::mediapipe::StatusOr>> input_status = + cc->Inputs().Index(i).Value().Consume>(); + if (input_status.ok()) { + std::unique_ptr> input_vector = + std::move(input_status).ValueOrDie(); + output->insert(output->end(), + std::make_move_iterator(input_vector->begin()), + std::make_move_iterator(input_vector->end())); + } else { + return input_status.status(); + } + } + cc->Outputs().Index(0).Add(output.release(), cc->InputTimestamp()); + return ::mediapipe::OkStatus(); + } + + template + ::mediapipe::Status ConsumeAndConcatenateVectors(std::false_type, + CalculatorContext* cc) { + return ::mediapipe::InternalError( + "Cannot copy or move input vectors to concatenate them"); + } + private: bool only_emit_if_all_present_; }; diff --git a/mediapipe/calculators/core/concatenate_vector_calculator_test.cc b/mediapipe/calculators/core/concatenate_vector_calculator_test.cc index 0baceaa26..4b27c2030 100644 --- a/mediapipe/calculators/core/concatenate_vector_calculator_test.cc +++ b/mediapipe/calculators/core/concatenate_vector_calculator_test.cc @@ -235,4 +235,167 @@ TEST(ConcatenateFloatVectorCalculatorTest, OneEmptyStreamNoOutput) { EXPECT_EQ(0, outputs.size()); } +typedef ConcatenateVectorCalculator> + TestConcatenateUniqueIntPtrCalculator; +REGISTER_CALCULATOR(TestConcatenateUniqueIntPtrCalculator); + +TEST(TestConcatenateUniqueIntVectorCalculatorTest, ConsumeOneTimestamp) { + /* Note: We don't use CalculatorRunner for this test because it keeps copies + * of input packets, so packets sent to the graph don't have sole ownership. + * The test needs to send packets that own the data. + */ + CalculatorGraphConfig graph_config = + ParseTextProtoOrDie(R"( + input_stream: "in_1" + input_stream: "in_2" + input_stream: "in_3" + node { + calculator: "TestConcatenateUniqueIntPtrCalculator" + input_stream: "in_1" + input_stream: "in_2" + input_stream: "in_3" + output_stream: "out" + } + )"); + + std::vector outputs; + tool::AddVectorSink("out", &graph_config, &outputs); + + CalculatorGraph graph; + MP_EXPECT_OK(graph.Initialize(graph_config)); + MP_EXPECT_OK(graph.StartRun({})); + + // input1 : {0, 1, 2} + std::unique_ptr>> input_1 = + absl::make_unique>>(3); + for (int i = 0; i < 3; ++i) { + input_1->at(i) = absl::make_unique(i); + } + // input2: {3} + std::unique_ptr>> input_2 = + absl::make_unique>>(1); + input_2->at(0) = absl::make_unique(3); + // input3: {4, 5} + std::unique_ptr>> input_3 = + absl::make_unique>>(2); + input_3->at(0) = absl::make_unique(4); + input_3->at(1) = absl::make_unique(5); + + MP_EXPECT_OK(graph.AddPacketToInputStream( + "in_1", Adopt(input_1.release()).At(Timestamp(1)))); + MP_EXPECT_OK(graph.AddPacketToInputStream( + "in_2", Adopt(input_2.release()).At(Timestamp(1)))); + MP_EXPECT_OK(graph.AddPacketToInputStream( + "in_3", Adopt(input_3.release()).At(Timestamp(1)))); + + MP_EXPECT_OK(graph.WaitUntilIdle()); + MP_EXPECT_OK(graph.CloseAllPacketSources()); + MP_EXPECT_OK(graph.WaitUntilDone()); + + EXPECT_EQ(1, outputs.size()); + EXPECT_EQ(Timestamp(1), outputs[0].Timestamp()); + const std::vector>& result = + outputs[0].Get>>(); + EXPECT_EQ(6, result.size()); + for (int i = 0; i < 6; ++i) { + const std::unique_ptr& v = result[i]; + EXPECT_EQ(i, *v); + } +} + +TEST(TestConcatenateUniqueIntVectorCalculatorTest, OneEmptyStreamStillOutput) { + /* Note: We don't use CalculatorRunner for this test because it keeps copies + * of input packets, so packets sent to the graph don't have sole ownership. + * The test needs to send packets that own the data. + */ + CalculatorGraphConfig graph_config = + ParseTextProtoOrDie(R"( + input_stream: "in_1" + input_stream: "in_2" + node { + calculator: "TestConcatenateUniqueIntPtrCalculator" + input_stream: "in_1" + input_stream: "in_2" + output_stream: "out" + } + )"); + + std::vector outputs; + tool::AddVectorSink("out", &graph_config, &outputs); + + CalculatorGraph graph; + MP_EXPECT_OK(graph.Initialize(graph_config)); + MP_EXPECT_OK(graph.StartRun({})); + + // input1 : {0, 1, 2} + std::unique_ptr>> input_1 = + absl::make_unique>>(3); + for (int i = 0; i < 3; ++i) { + input_1->at(i) = absl::make_unique(i); + } + + MP_EXPECT_OK(graph.AddPacketToInputStream( + "in_1", Adopt(input_1.release()).At(Timestamp(1)))); + + MP_EXPECT_OK(graph.WaitUntilIdle()); + MP_EXPECT_OK(graph.CloseAllPacketSources()); + MP_EXPECT_OK(graph.WaitUntilDone()); + + EXPECT_EQ(1, outputs.size()); + EXPECT_EQ(Timestamp(1), outputs[0].Timestamp()); + const std::vector>& result = + outputs[0].Get>>(); + EXPECT_EQ(3, result.size()); + for (int i = 0; i < 3; ++i) { + const std::unique_ptr& v = result[i]; + EXPECT_EQ(i, *v); + } +} + +TEST(TestConcatenateUniqueIntVectorCalculatorTest, OneEmptyStreamNoOutput) { + /* Note: We don't use CalculatorRunner for this test because it keeps copies + * of input packets, so packets sent to the graph don't have sole ownership. + * The test needs to send packets that own the data. + */ + CalculatorGraphConfig graph_config = + ParseTextProtoOrDie(R"( + input_stream: "in_1" + input_stream: "in_2" + node { + calculator: "TestConcatenateUniqueIntPtrCalculator" + input_stream: "in_1" + input_stream: "in_2" + output_stream: "out" + options { + [mediapipe.ConcatenateVectorCalculatorOptions.ext] { + only_emit_if_all_present: true + } + } + } + )"); + + std::vector outputs; + tool::AddVectorSink("out", &graph_config, &outputs); + + CalculatorGraph graph; + MP_EXPECT_OK(graph.Initialize(graph_config)); + MP_EXPECT_OK(graph.StartRun({})); + + // input1 : {0, 1, 2} + std::unique_ptr>> input_1 = + absl::make_unique>>(3); + for (int i = 0; i < 3; ++i) { + input_1->at(i) = absl::make_unique(i); + } + + MP_EXPECT_OK(graph.AddPacketToInputStream( + "in_1", Adopt(input_1.release()).At(Timestamp(1)))); + + MP_EXPECT_OK(graph.WaitUntilIdle()); + MP_EXPECT_OK(graph.CloseAllPacketSources()); + MP_EXPECT_OK(graph.WaitUntilDone()); + + EXPECT_EQ(0, outputs.size()); +} + } // namespace mediapipe diff --git a/mediapipe/calculators/image/BUILD b/mediapipe/calculators/image/BUILD index 3773a2180..b8fdbdfae 100644 --- a/mediapipe/calculators/image/BUILD +++ b/mediapipe/calculators/image/BUILD @@ -19,7 +19,6 @@ package(default_visibility = ["//visibility:private"]) exports_files(["LICENSE"]) load("//mediapipe/framework/port:build_config.bzl", "mediapipe_cc_proto_library") -load("@bazel_skylib//lib:selects.bzl", "selects") proto_library( name = "opencv_image_encoder_calculator_proto", @@ -227,19 +226,13 @@ cc_library( "//mediapipe/framework/port:status", "//mediapipe/framework/port:vector", ] + select({ - "//mediapipe:android": [ + "//mediapipe/gpu:disable_gpu": [], + "//conditions:default": [ "//mediapipe/gpu:gl_calculator_helper", "//mediapipe/gpu:gl_simple_shaders", - "//mediapipe/gpu:gpu_buffer", + "//mediapipe/gpu:gl_quad_renderer", "//mediapipe/gpu:shader_util", ], - "//mediapipe:ios": [ - "//mediapipe/gpu:gl_calculator_helper", - "//mediapipe/gpu:gl_simple_shaders", - "//mediapipe/gpu:gpu_buffer", - "//mediapipe/gpu:shader_util", - ], - "//conditions:default": [], }), alwayslink = 1, ) @@ -263,13 +256,13 @@ cc_library( "//mediapipe/framework/port:status", "//mediapipe/framework/port:vector", ] + select({ - "//mediapipe:android": [ + "//mediapipe/gpu:disable_gpu": [], + "//conditions:default": [ "//mediapipe/gpu:gl_calculator_helper", "//mediapipe/gpu:gl_simple_shaders", - "//mediapipe/gpu:gpu_buffer", + "//mediapipe/gpu:gl_quad_renderer", "//mediapipe/gpu:shader_util", ], - "//conditions:default": [], }), alwayslink = 1, ) @@ -322,14 +315,14 @@ cc_library( "//mediapipe/framework/port:opencv_imgproc", "//mediapipe/framework/port:ret_check", "//mediapipe/framework/port:status", - ] + selects.with_or({ - ("//mediapipe:android", "//mediapipe:ios"): [ + ] + select({ + "//mediapipe/gpu:disable_gpu": [], + "//conditions:default": [ "//mediapipe/gpu:gl_calculator_helper", "//mediapipe/gpu:gl_simple_shaders", "//mediapipe/gpu:gl_quad_renderer", "//mediapipe/gpu:shader_util", ], - "//conditions:default": [], }), alwayslink = 1, ) @@ -363,14 +356,15 @@ cc_library( "//mediapipe/framework/port:opencv_imgproc", "//mediapipe/framework/port:ret_check", "//mediapipe/framework/port:status", - ] + selects.with_or({ - ("//mediapipe:android", "//mediapipe:ios"): [ + "//mediapipe/gpu:gpu_buffer", + ] + select({ + "//mediapipe/gpu:disable_gpu": [], + "//conditions:default": [ "//mediapipe/gpu:gl_calculator_helper", "//mediapipe/gpu:gl_simple_shaders", "//mediapipe/gpu:gl_quad_renderer", "//mediapipe/gpu:shader_util", ], - "//conditions:default": [], }), alwayslink = 1, ) @@ -415,19 +409,13 @@ cc_library( "//mediapipe/framework/port:ret_check", "//mediapipe/util:color_cc_proto", ] + select({ - "//mediapipe:android": [ + "//mediapipe/gpu:disable_gpu": [], + "//conditions:default": [ "//mediapipe/gpu:gl_calculator_helper", "//mediapipe/gpu:gl_simple_shaders", - "//mediapipe/gpu:gpu_buffer", + "//mediapipe/gpu:gl_quad_renderer", "//mediapipe/gpu:shader_util", ], - "//mediapipe:ios": [ - "//mediapipe/gpu:gl_calculator_helper", - "//mediapipe/gpu:gl_simple_shaders", - "//mediapipe/gpu:gpu_buffer", - "//mediapipe/gpu:shader_util", - ], - "//conditions:default": [], }), alwayslink = 1, ) @@ -486,11 +474,11 @@ cc_library( "//mediapipe/framework/formats:image_frame", "//mediapipe/framework/port:ret_check", "//mediapipe/framework/port:status", - ] + selects.with_or({ - ("//mediapipe:android", "//mediapipe:ios"): [ + ] + select({ + "//mediapipe/gpu:disable_gpu": [], + "//conditions:default": [ "//mediapipe/gpu:gpu_buffer", ], - "//conditions:default": [], }), alwayslink = 1, ) diff --git a/mediapipe/calculators/image/bilateral_filter_calculator.cc b/mediapipe/calculators/image/bilateral_filter_calculator.cc index 0adb9390a..e1d26c1e0 100644 --- a/mediapipe/calculators/image/bilateral_filter_calculator.cc +++ b/mediapipe/calculators/image/bilateral_filter_calculator.cc @@ -27,11 +27,11 @@ #include "mediapipe/framework/port/status.h" #include "mediapipe/framework/port/vector.h" -#if defined(__ANDROID__) || defined(__EMSCRIPTEN__) +#if !defined(MEDIAPIPE_DISABLE_GPU) #include "mediapipe/gpu/gl_calculator_helper.h" #include "mediapipe/gpu/gl_simple_shaders.h" #include "mediapipe/gpu/shader_util.h" -#endif // __ANDROID__ || __EMSCRIPTEN__ +#endif // !MEDIAPIPE_DISABLE_GPU namespace mediapipe { @@ -101,11 +101,11 @@ class BilateralFilterCalculator : public CalculatorBase { bool use_gpu_ = false; bool gpu_initialized_ = false; -#if defined(__ANDROID__) || defined(__EMSCRIPTEN__) +#if !defined(MEDIAPIPE_DISABLE_GPU) mediapipe::GlCalculatorHelper gpu_helper_; GLuint program_ = 0; GLuint program_joint_ = 0; -#endif // __ANDROID__ || __EMSCRIPTEN__ +#endif // !MEDIAPIPE_DISABLE_GPU }; REGISTER_CALCULATOR(BilateralFilterCalculator); @@ -122,39 +122,46 @@ REGISTER_CALCULATOR(BilateralFilterCalculator); return ::mediapipe::InternalError("GPU output must have GPU input."); } + bool use_gpu = false; + // Input image to filter. -#if defined(__ANDROID__) || defined(__EMSCRIPTEN__) +#if !defined(MEDIAPIPE_DISABLE_GPU) if (cc->Inputs().HasTag(kInputFrameTagGpu)) { cc->Inputs().Tag(kInputFrameTagGpu).Set(); + use_gpu |= true; } -#endif // __ANDROID__ || __EMSCRIPTEN__ +#endif // !MEDIAPIPE_DISABLE_GPU if (cc->Inputs().HasTag(kInputFrameTag)) { cc->Inputs().Tag(kInputFrameTag).Set(); } // Input guide image mask (optional) +#if !defined(MEDIAPIPE_DISABLE_GPU) if (cc->Inputs().HasTag(kInputGuideTagGpu)) { -#if defined(__ANDROID__) || defined(__EMSCRIPTEN__) cc->Inputs().Tag(kInputGuideTagGpu).Set(); -#endif // __ANDROID__ || __EMSCRIPTEN__ + use_gpu |= true; } +#endif // !MEDIAPIPE_DISABLE_GPU if (cc->Inputs().HasTag(kInputGuideTag)) { cc->Inputs().Tag(kInputGuideTag).Set(); } // Output image. -#if defined(__ANDROID__) || defined(__EMSCRIPTEN__) +#if !defined(MEDIAPIPE_DISABLE_GPU) if (cc->Outputs().HasTag(kOutputFrameTagGpu)) { cc->Outputs().Tag(kOutputFrameTagGpu).Set(); + use_gpu |= true; } -#endif // __ANDROID__ || __EMSCRIPTEN__ +#endif // !MEDIAPIPE_DISABLE_GPU if (cc->Outputs().HasTag(kOutputFrameTag)) { cc->Outputs().Tag(kOutputFrameTag).Set(); } -#if defined(__ANDROID__) || defined(__EMSCRIPTEN__) - MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); -#endif // __ANDROID__ || __EMSCRIPTEN__ + if (use_gpu) { +#if !defined(MEDIAPIPE_DISABLE_GPU) + MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); +#endif // !MEDIAPIPE_DISABLE_GPU + } return ::mediapipe::OkStatus(); } @@ -166,11 +173,11 @@ REGISTER_CALCULATOR(BilateralFilterCalculator); if (cc->Inputs().HasTag(kInputFrameTagGpu) && cc->Outputs().HasTag(kOutputFrameTagGpu)) { -#if defined(__ANDROID__) || defined(__EMSCRIPTEN__) +#if !defined(MEDIAPIPE_DISABLE_GPU) use_gpu_ = true; #else - RET_CHECK_FAIL() << "GPU processing on non-Android not supported yet."; -#endif // __ANDROID__ || __EMSCRIPTEN__ + RET_CHECK_FAIL() << "GPU processing not enabled."; +#endif } sigma_color_ = options_.sigma_color(); @@ -180,9 +187,9 @@ REGISTER_CALCULATOR(BilateralFilterCalculator); if (!use_gpu_) sigma_color_ *= 255.0; if (use_gpu_) { -#if defined(__ANDROID__) || defined(__EMSCRIPTEN__) +#if !defined(MEDIAPIPE_DISABLE_GPU) MP_RETURN_IF_ERROR(gpu_helper_.Open(cc)); -#endif +#endif // !MEDIAPIPE_DISABLE_GPU } return ::mediapipe::OkStatus(); @@ -190,7 +197,7 @@ REGISTER_CALCULATOR(BilateralFilterCalculator); ::mediapipe::Status BilateralFilterCalculator::Process(CalculatorContext* cc) { if (use_gpu_) { -#if defined(__ANDROID__) || defined(__EMSCRIPTEN__) +#if !defined(MEDIAPIPE_DISABLE_GPU) MP_RETURN_IF_ERROR( gpu_helper_.RunInGlContext([this, cc]() -> ::mediapipe::Status { if (!gpu_initialized_) { @@ -200,7 +207,7 @@ REGISTER_CALCULATOR(BilateralFilterCalculator); MP_RETURN_IF_ERROR(RenderGpu(cc)); return ::mediapipe::OkStatus(); })); -#endif // __ANDROID__ || __EMSCRIPTEN__ +#endif // !MEDIAPIPE_DISABLE_GPU } else { MP_RETURN_IF_ERROR(RenderCpu(cc)); } @@ -209,14 +216,14 @@ REGISTER_CALCULATOR(BilateralFilterCalculator); } ::mediapipe::Status BilateralFilterCalculator::Close(CalculatorContext* cc) { -#if defined(__ANDROID__) || defined(__EMSCRIPTEN__) +#if !defined(MEDIAPIPE_DISABLE_GPU) gpu_helper_.RunInGlContext([this] { if (program_) glDeleteProgram(program_); program_ = 0; if (program_joint_) glDeleteProgram(program_joint_); program_joint_ = 0; }); -#endif // __ANDROID__ || __EMSCRIPTEN__ +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } @@ -263,7 +270,7 @@ REGISTER_CALCULATOR(BilateralFilterCalculator); if (cc->Inputs().Tag(kInputFrameTagGpu).IsEmpty()) { return ::mediapipe::OkStatus(); } -#if defined(__ANDROID__) || defined(__EMSCRIPTEN__) +#if !defined(MEDIAPIPE_DISABLE_GPU) const auto& input_frame = cc->Inputs().Tag(kInputFrameTagGpu).Get(); auto input_texture = gpu_helper_.CreateSourceTexture(input_frame); @@ -321,13 +328,13 @@ REGISTER_CALCULATOR(BilateralFilterCalculator); // Cleanup input_texture.Release(); output_texture.Release(); -#endif // __ANDROID__ || __EMSCRIPTEN__ +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } void BilateralFilterCalculator::GlRender(CalculatorContext* cc) { -#if defined(__ANDROID__) || defined(__EMSCRIPTEN__) +#if !defined(MEDIAPIPE_DISABLE_GPU) static const GLfloat square_vertices[] = { -1.0f, -1.0f, // bottom left 1.0f, -1.0f, // bottom right @@ -373,11 +380,11 @@ void BilateralFilterCalculator::GlRender(CalculatorContext* cc) { glDeleteVertexArrays(1, &vao); glDeleteBuffers(2, vbo); -#endif // __ANDROID__ || __EMSCRIPTEN__ +#endif // !MEDIAPIPE_DISABLE_GPU } ::mediapipe::Status BilateralFilterCalculator::GlSetup(CalculatorContext* cc) { -#if defined(__ANDROID__) || defined(__EMSCRIPTEN__) +#if !defined(MEDIAPIPE_DISABLE_GPU) const GLint attr_location[NUM_ATTRIBUTES] = { ATTRIB_VERTEX, ATTRIB_TEXTURE_POSITION, @@ -545,7 +552,7 @@ void BilateralFilterCalculator::GlRender(CalculatorContext* cc) { glUniform1i(glGetUniformLocation(program_joint_, "input_frame"), 1); glUniform1i(glGetUniformLocation(program_joint_, "guide_frame"), 2); -#endif // __ANDROID__ || __EMSCRIPTEN__ +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } diff --git a/mediapipe/calculators/image/image_cropping_calculator.cc b/mediapipe/calculators/image/image_cropping_calculator.cc index b893cd260..a9277e871 100644 --- a/mediapipe/calculators/image/image_cropping_calculator.cc +++ b/mediapipe/calculators/image/image_cropping_calculator.cc @@ -24,12 +24,12 @@ #include "mediapipe/framework/port/ret_check.h" #include "mediapipe/framework/port/status.h" -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) #include "mediapipe/gpu/gl_calculator_helper.h" #include "mediapipe/gpu/gl_simple_shaders.h" #include "mediapipe/gpu/gpu_buffer.h" #include "mediapipe/gpu/shader_util.h" -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU namespace { enum { ATTRIB_VERTEX, ATTRIB_TEXTURE_POSITION, NUM_ATTRIBUTES }; @@ -37,9 +37,20 @@ enum { ATTRIB_VERTEX, ATTRIB_TEXTURE_POSITION, NUM_ATTRIBUTES }; namespace mediapipe { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +namespace { -#endif // __ANDROID__ or iOS +#if !defined(MEDIAPIPE_DISABLE_GPU) + +#endif // !MEDIAPIPE_DISABLE_GPU + +constexpr char kRectTag[] = "RECT"; +constexpr char kNormRectTag[] = "NORM_RECT"; +constexpr char kHeightTag[] = "HEIGHT"; +constexpr char kImageTag[] = "IMAGE"; +constexpr char kImageGpuTag[] = "IMAGE_GPU"; +constexpr char kWidthTag[] = "WIDTH"; + +} // namespace // Crops the input texture to the given rectangle region. The rectangle can // be at arbitrary location on the image with rotation. If there's rotation, the @@ -91,48 +102,55 @@ class ImageCroppingCalculator : public CalculatorBase { bool use_gpu_ = false; // Output texture corners (4) after transoformation in normalized coordinates. float transformed_points_[8]; -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) bool gpu_initialized_ = false; mediapipe::GlCalculatorHelper gpu_helper_; GLuint program_ = 0; -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU }; REGISTER_CALCULATOR(ImageCroppingCalculator); ::mediapipe::Status ImageCroppingCalculator::GetContract( CalculatorContract* cc) { - RET_CHECK(cc->Inputs().HasTag("IMAGE") ^ cc->Inputs().HasTag("IMAGE_GPU")); - RET_CHECK(cc->Outputs().HasTag("IMAGE") ^ cc->Outputs().HasTag("IMAGE_GPU")); + RET_CHECK(cc->Inputs().HasTag(kImageTag) ^ cc->Inputs().HasTag(kImageGpuTag)); + RET_CHECK(cc->Outputs().HasTag(kImageTag) ^ + cc->Outputs().HasTag(kImageGpuTag)); - if (cc->Inputs().HasTag("IMAGE")) { - RET_CHECK(cc->Outputs().HasTag("IMAGE")); - cc->Inputs().Tag("IMAGE").Set(); - cc->Outputs().Tag("IMAGE").Set(); - } -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) - if (cc->Inputs().HasTag("IMAGE_GPU")) { - RET_CHECK(cc->Outputs().HasTag("IMAGE_GPU")); - cc->Inputs().Tag("IMAGE_GPU").Set(); - cc->Outputs().Tag("IMAGE_GPU").Set(); - } -#endif // __ANDROID__ or iOS + bool use_gpu = false; - if (cc->Inputs().HasTag("RECT")) { - cc->Inputs().Tag("RECT").Set(); + if (cc->Inputs().HasTag(kImageTag)) { + RET_CHECK(cc->Outputs().HasTag(kImageTag)); + cc->Inputs().Tag(kImageTag).Set(); + cc->Outputs().Tag(kImageTag).Set(); } - if (cc->Inputs().HasTag("NORM_RECT")) { - cc->Inputs().Tag("NORM_RECT").Set(); +#if !defined(MEDIAPIPE_DISABLE_GPU) + if (cc->Inputs().HasTag(kImageGpuTag)) { + RET_CHECK(cc->Outputs().HasTag(kImageGpuTag)); + cc->Inputs().Tag(kImageGpuTag).Set(); + cc->Outputs().Tag(kImageGpuTag).Set(); + use_gpu |= true; } - if (cc->Inputs().HasTag("WIDTH")) { - cc->Inputs().Tag("WIDTH").Set(); +#endif // !MEDIAPIPE_DISABLE_GPU + + RET_CHECK(cc->Inputs().HasTag(kRectTag) ^ cc->Inputs().HasTag(kNormRectTag)); + if (cc->Inputs().HasTag(kRectTag)) { + cc->Inputs().Tag(kRectTag).Set(); } - if (cc->Inputs().HasTag("HEIGHT")) { - cc->Inputs().Tag("HEIGHT").Set(); + if (cc->Inputs().HasTag(kNormRectTag)) { + cc->Inputs().Tag(kNormRectTag).Set(); + } + if (cc->Inputs().HasTag(kWidthTag)) { + cc->Inputs().Tag(kWidthTag).Set(); + } + if (cc->Inputs().HasTag(kHeightTag)) { + cc->Inputs().Tag(kHeightTag).Set(); } -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) - MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); -#endif // __ANDROID__ or iOS + if (use_gpu) { +#if !defined(MEDIAPIPE_DISABLE_GPU) + MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); +#endif // !MEDIAPIPE_DISABLE_GPU + } return ::mediapipe::OkStatus(); } @@ -140,26 +158,35 @@ REGISTER_CALCULATOR(ImageCroppingCalculator); ::mediapipe::Status ImageCroppingCalculator::Open(CalculatorContext* cc) { cc->SetOffset(TimestampDiff(0)); - if (cc->Inputs().HasTag("IMAGE_GPU")) { + if (cc->Inputs().HasTag(kImageGpuTag)) { use_gpu_ = true; } options_ = cc->Options(); if (use_gpu_) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) MP_RETURN_IF_ERROR(gpu_helper_.Open(cc)); #else RET_CHECK_FAIL() << "GPU processing is for Android and iOS only."; -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU } return ::mediapipe::OkStatus(); } ::mediapipe::Status ImageCroppingCalculator::Process(CalculatorContext* cc) { + if (cc->Inputs().HasTag(kRectTag) && cc->Inputs().Tag(kRectTag).IsEmpty()) { + VLOG(1) << "RECT is empty for timestamp: " << cc->InputTimestamp(); + return ::mediapipe::OkStatus(); + } + if (cc->Inputs().HasTag(kNormRectTag) && + cc->Inputs().Tag(kNormRectTag).IsEmpty()) { + VLOG(1) << "NORM_RECT is empty for timestamp: " << cc->InputTimestamp(); + return ::mediapipe::OkStatus(); + } if (use_gpu_) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) MP_RETURN_IF_ERROR( gpu_helper_.RunInGlContext([this, cc]() -> ::mediapipe::Status { if (!gpu_initialized_) { @@ -169,7 +196,7 @@ REGISTER_CALCULATOR(ImageCroppingCalculator); MP_RETURN_IF_ERROR(RenderGpu(cc)); return ::mediapipe::OkStatus(); })); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU } else { MP_RETURN_IF_ERROR(RenderCpu(cc)); } @@ -177,19 +204,22 @@ REGISTER_CALCULATOR(ImageCroppingCalculator); } ::mediapipe::Status ImageCroppingCalculator::Close(CalculatorContext* cc) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) gpu_helper_.RunInGlContext([this] { if (program_) glDeleteProgram(program_); program_ = 0; }); gpu_initialized_ = false; -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } ::mediapipe::Status ImageCroppingCalculator::RenderCpu(CalculatorContext* cc) { - const auto& input_img = cc->Inputs().Tag("IMAGE").Get(); + if (cc->Inputs().Tag(kImageTag).IsEmpty()) { + return ::mediapipe::OkStatus(); + } + const auto& input_img = cc->Inputs().Tag(kImageTag).Get(); cv::Mat input_mat = formats::MatView(&input_img); float rect_center_x = input_img.Width() / 2.0f; @@ -197,8 +227,8 @@ REGISTER_CALCULATOR(ImageCroppingCalculator); float rotation = 0.0f; int target_width = input_img.Width(); int target_height = input_img.Height(); - if (cc->Inputs().HasTag("RECT")) { - const auto& rect = cc->Inputs().Tag("RECT").Get(); + if (cc->Inputs().HasTag(kRectTag)) { + const auto& rect = cc->Inputs().Tag(kRectTag).Get(); if (rect.width() > 0 && rect.height() > 0 && rect.x_center() >= 0 && rect.y_center() >= 0) { rect_center_x = rect.x_center(); @@ -207,8 +237,8 @@ REGISTER_CALCULATOR(ImageCroppingCalculator); target_height = rect.height(); rotation = rect.rotation(); } - } else if (cc->Inputs().HasTag("NORM_RECT")) { - const auto& rect = cc->Inputs().Tag("NORM_RECT").Get(); + } else if (cc->Inputs().HasTag(kNormRectTag)) { + const auto& rect = cc->Inputs().Tag(kNormRectTag).Get(); if (rect.width() > 0.0 && rect.height() > 0.0 && rect.x_center() >= 0.0 && rect.y_center() >= 0.0) { rect_center_x = std::round(rect.x_center() * input_img.Width()); @@ -218,9 +248,9 @@ REGISTER_CALCULATOR(ImageCroppingCalculator); rotation = rect.rotation(); } } else { - if (cc->Inputs().HasTag("WIDTH") && cc->Inputs().HasTag("HEIGHT")) { - target_width = cc->Inputs().Tag("WIDTH").Get(); - target_height = cc->Inputs().Tag("HEIGHT").Get(); + if (cc->Inputs().HasTag(kWidthTag) && cc->Inputs().HasTag(kHeightTag)) { + target_width = cc->Inputs().Tag(kWidthTag).Get(); + target_height = cc->Inputs().Tag(kHeightTag).Get(); } else if (options_.has_width() && options_.has_height()) { target_width = options_.width(); target_height = options_.height(); @@ -253,16 +283,17 @@ REGISTER_CALCULATOR(ImageCroppingCalculator); input_img.Format(), cropped_image.cols, cropped_image.rows)); cv::Mat output_mat = formats::MatView(output_frame.get()); cropped_image.copyTo(output_mat); - cc->Outputs().Tag("IMAGE").Add(output_frame.release(), cc->InputTimestamp()); + cc->Outputs().Tag(kImageTag).Add(output_frame.release(), + cc->InputTimestamp()); return ::mediapipe::OkStatus(); } ::mediapipe::Status ImageCroppingCalculator::RenderGpu(CalculatorContext* cc) { - if (cc->Inputs().Tag("IMAGE_GPU").IsEmpty()) { + if (cc->Inputs().Tag(kImageGpuTag).IsEmpty()) { return ::mediapipe::OkStatus(); } -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) - const Packet& input_packet = cc->Inputs().Tag("IMAGE_GPU").Value(); +#if !defined(MEDIAPIPE_DISABLE_GPU) + const Packet& input_packet = cc->Inputs().Tag(kImageGpuTag).Value(); const auto& input_buffer = input_packet.Get(); auto src_tex = gpu_helper_.CreateSourceTexture(input_buffer); @@ -287,18 +318,18 @@ REGISTER_CALCULATOR(ImageCroppingCalculator); // Send result image in GPU packet. auto output = dst_tex.GetFrame(); - cc->Outputs().Tag("IMAGE_GPU").Add(output.release(), cc->InputTimestamp()); + cc->Outputs().Tag(kImageGpuTag).Add(output.release(), cc->InputTimestamp()); // Cleanup src_tex.Release(); dst_tex.Release(); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } void ImageCroppingCalculator::GlRender() { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) static const GLfloat square_vertices[] = { -1.0f, -1.0f, // bottom left 1.0f, -1.0f, // bottom right @@ -342,11 +373,11 @@ void ImageCroppingCalculator::GlRender() { glDeleteVertexArrays(1, &vao); glDeleteBuffers(2, vbo); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU } ::mediapipe::Status ImageCroppingCalculator::InitGpu(CalculatorContext* cc) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) const GLint attr_location[NUM_ATTRIBUTES] = { ATTRIB_VERTEX, ATTRIB_TEXTURE_POSITION, @@ -392,7 +423,7 @@ void ImageCroppingCalculator::GlRender() { // Parameters glUseProgram(program_); glUniform1i(glGetUniformLocation(program_, "input_frame"), 1); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } @@ -410,8 +441,8 @@ void ImageCroppingCalculator::GetOutputDimensions(CalculatorContext* cc, int y_center = src_height / 2; // Get the rotation of the cropping box. float rotation = 0.0f; - if (cc->Inputs().HasTag("RECT")) { - const auto& rect = cc->Inputs().Tag("RECT").Get(); + if (cc->Inputs().HasTag(kRectTag)) { + const auto& rect = cc->Inputs().Tag(kRectTag).Get(); // Only use the rect if it is valid. if (rect.width() > 0 && rect.height() > 0 && rect.x_center() >= 0 && rect.y_center() >= 0) { @@ -421,8 +452,8 @@ void ImageCroppingCalculator::GetOutputDimensions(CalculatorContext* cc, crop_height = rect.height(); rotation = rect.rotation(); } - } else if (cc->Inputs().HasTag("NORM_RECT")) { - const auto& rect = cc->Inputs().Tag("NORM_RECT").Get(); + } else if (cc->Inputs().HasTag(kNormRectTag)) { + const auto& rect = cc->Inputs().Tag(kNormRectTag).Get(); // Only use the rect if it is valid. if (rect.width() > 0.0 && rect.height() > 0.0 && rect.x_center() >= 0.0 && rect.y_center() >= 0.0) { @@ -433,9 +464,9 @@ void ImageCroppingCalculator::GetOutputDimensions(CalculatorContext* cc, rotation = rect.rotation(); } } else { - if (cc->Inputs().HasTag("WIDTH") && cc->Inputs().HasTag("HEIGHT")) { - crop_width = cc->Inputs().Tag("WIDTH").Get(); - crop_height = cc->Inputs().Tag("HEIGHT").Get(); + if (cc->Inputs().HasTag(kWidthTag) && cc->Inputs().HasTag(kHeightTag)) { + crop_width = cc->Inputs().Tag(kWidthTag).Get(); + crop_height = cc->Inputs().Tag(kHeightTag).Get(); } else if (options_.has_width() && options_.has_height()) { crop_width = options_.width(); crop_height = options_.height(); diff --git a/mediapipe/calculators/image/image_properties_calculator.cc b/mediapipe/calculators/image/image_properties_calculator.cc index 70c49de61..ea6c06c43 100644 --- a/mediapipe/calculators/image/image_properties_calculator.cc +++ b/mediapipe/calculators/image/image_properties_calculator.cc @@ -15,9 +15,9 @@ #include "mediapipe/framework/calculator_framework.h" #include "mediapipe/framework/formats/image_frame.h" -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) #include "mediapipe/gpu/gpu_buffer.h" -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU namespace mediapipe { @@ -44,11 +44,11 @@ class ImagePropertiesCalculator : public CalculatorBase { if (cc->Inputs().HasTag("IMAGE")) { cc->Inputs().Tag("IMAGE").Set(); } -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) if (cc->Inputs().HasTag("IMAGE_GPU")) { cc->Inputs().Tag("IMAGE_GPU").Set<::mediapipe::GpuBuffer>(); } -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU if (cc->Outputs().HasTag("SIZE")) { cc->Outputs().Tag("SIZE").Set>(); @@ -71,7 +71,7 @@ class ImagePropertiesCalculator : public CalculatorBase { width = image.Width(); height = image.Height(); } -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) if (cc->Inputs().HasTag("IMAGE_GPU") && !cc->Inputs().Tag("IMAGE_GPU").IsEmpty()) { const auto& image = @@ -79,7 +79,7 @@ class ImagePropertiesCalculator : public CalculatorBase { width = image.width(); height = image.height(); } -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU cc->Outputs().Tag("SIZE").AddPacket( MakePacket>(width, height) diff --git a/mediapipe/calculators/image/image_transformation_calculator.cc b/mediapipe/calculators/image/image_transformation_calculator.cc index c2d894547..5eb34c3c0 100644 --- a/mediapipe/calculators/image/image_transformation_calculator.cc +++ b/mediapipe/calculators/image/image_transformation_calculator.cc @@ -22,12 +22,12 @@ #include "mediapipe/framework/port/status.h" #include "mediapipe/gpu/scale_mode.pb.h" -#if defined(__ANDROID__) || defined(__APPLE__) && !TARGET_OS_OSX +#if !defined(MEDIAPIPE_DISABLE_GPU) #include "mediapipe/gpu/gl_calculator_helper.h" #include "mediapipe/gpu/gl_quad_renderer.h" #include "mediapipe/gpu/gl_simple_shaders.h" #include "mediapipe/gpu/shader_util.h" -#endif // __ANDROID__ || iOS +#endif // !MEDIAPIPE_DISABLE_GPU #if defined(__ANDROID__) // The size of Java arrays is dynamic, which makes it difficult to @@ -42,9 +42,9 @@ typedef int DimensionsPacketType[2]; namespace mediapipe { -#if defined(__ANDROID__) || defined(__APPLE__) && !TARGET_OS_OSX +#if !defined(MEDIAPIPE_DISABLE_GPU) -#endif // __ANDROID__ || iOS +#endif // !MEDIAPIPE_DISABLE_GPU namespace { int RotationModeToDegrees(mediapipe::RotationMode_Mode rotation) { @@ -170,12 +170,12 @@ class ImageTransformationCalculator : public CalculatorBase { mediapipe::ScaleMode_Mode scale_mode_; bool use_gpu_ = false; -#if defined(__ANDROID__) || defined(__APPLE__) && !TARGET_OS_OSX +#if !defined(MEDIAPIPE_DISABLE_GPU) GlCalculatorHelper helper_; std::unique_ptr rgb_renderer_; std::unique_ptr yuv_renderer_; std::unique_ptr ext_rgb_renderer_; -#endif // __ANDROID__ || iOS +#endif // !MEDIAPIPE_DISABLE_GPU }; REGISTER_CALCULATOR(ImageTransformationCalculator); @@ -185,18 +185,22 @@ REGISTER_CALCULATOR(ImageTransformationCalculator); RET_CHECK(cc->Inputs().HasTag("IMAGE") ^ cc->Inputs().HasTag("IMAGE_GPU")); RET_CHECK(cc->Outputs().HasTag("IMAGE") ^ cc->Outputs().HasTag("IMAGE_GPU")); + bool use_gpu = false; + if (cc->Inputs().HasTag("IMAGE")) { RET_CHECK(cc->Outputs().HasTag("IMAGE")); cc->Inputs().Tag("IMAGE").Set(); cc->Outputs().Tag("IMAGE").Set(); } -#if defined(__ANDROID__) || defined(__APPLE__) && !TARGET_OS_OSX +#if !defined(MEDIAPIPE_DISABLE_GPU) if (cc->Inputs().HasTag("IMAGE_GPU")) { RET_CHECK(cc->Outputs().HasTag("IMAGE_GPU")); cc->Inputs().Tag("IMAGE_GPU").Set(); cc->Outputs().Tag("IMAGE_GPU").Set(); + use_gpu |= true; } -#endif // __ANDROID__ || iOS +#endif // !MEDIAPIPE_DISABLE_GPU + if (cc->Inputs().HasTag("ROTATION_DEGREES")) { cc->Inputs().Tag("ROTATION_DEGREES").Set(); } @@ -212,9 +216,11 @@ REGISTER_CALCULATOR(ImageTransformationCalculator); cc->Outputs().Tag("LETTERBOX_PADDING").Set>(); } -#if defined(__ANDROID__) || defined(__APPLE__) && !TARGET_OS_OSX - MP_RETURN_IF_ERROR(GlCalculatorHelper::UpdateContract(cc)); -#endif // __ANDROID__ || iOS + if (use_gpu) { +#if !defined(MEDIAPIPE_DISABLE_GPU) + MP_RETURN_IF_ERROR(GlCalculatorHelper::UpdateContract(cc)); +#endif // !MEDIAPIPE_DISABLE_GPU + } return ::mediapipe::OkStatus(); } @@ -250,12 +256,12 @@ REGISTER_CALCULATOR(ImageTransformationCalculator); scale_mode_ = ParseScaleMode(options_.scale_mode(), DEFAULT_SCALE_MODE); if (use_gpu_) { -#if defined(__ANDROID__) || defined(__APPLE__) && !TARGET_OS_OSX +#if !defined(MEDIAPIPE_DISABLE_GPU) // Let the helper access the GL context information. MP_RETURN_IF_ERROR(helper_.Open(cc)); #else - RET_CHECK_FAIL() << "GPU processing is for Android and iOS only."; -#endif // __ANDROID__ || iOS + RET_CHECK_FAIL() << "GPU processing not enabled."; +#endif // !MEDIAPIPE_DISABLE_GPU } return ::mediapipe::OkStatus(); @@ -264,10 +270,10 @@ REGISTER_CALCULATOR(ImageTransformationCalculator); ::mediapipe::Status ImageTransformationCalculator::Process( CalculatorContext* cc) { if (use_gpu_) { -#if defined(__ANDROID__) || defined(__APPLE__) && !TARGET_OS_OSX +#if !defined(MEDIAPIPE_DISABLE_GPU) return helper_.RunInGlContext( [this, cc]() -> ::mediapipe::Status { return RenderGpu(cc); }); -#endif // __ANDROID__ || iOS +#endif // !MEDIAPIPE_DISABLE_GPU } else { return RenderCpu(cc); } @@ -277,7 +283,7 @@ REGISTER_CALCULATOR(ImageTransformationCalculator); ::mediapipe::Status ImageTransformationCalculator::Close( CalculatorContext* cc) { if (use_gpu_) { -#if defined(__ANDROID__) || defined(__APPLE__) && !TARGET_OS_OSX +#if !defined(MEDIAPIPE_DISABLE_GPU) QuadRenderer* rgb_renderer = rgb_renderer_.release(); QuadRenderer* yuv_renderer = yuv_renderer_.release(); QuadRenderer* ext_rgb_renderer = ext_rgb_renderer_.release(); @@ -295,8 +301,9 @@ REGISTER_CALCULATOR(ImageTransformationCalculator); delete yuv_renderer; } }); -#endif // __ANDROID__ || iOS +#endif // !MEDIAPIPE_DISABLE_GPU } + return ::mediapipe::OkStatus(); } @@ -371,7 +378,7 @@ REGISTER_CALCULATOR(ImageTransformationCalculator); ::mediapipe::Status ImageTransformationCalculator::RenderGpu( CalculatorContext* cc) { -#if defined(__ANDROID__) || defined(__APPLE__) && !TARGET_OS_OSX +#if !defined(MEDIAPIPE_DISABLE_GPU) int input_width = cc->Inputs().Tag("IMAGE_GPU").Get().width(); int input_height = cc->Inputs().Tag("IMAGE_GPU").Get().height(); @@ -408,7 +415,7 @@ REGISTER_CALCULATOR(ImageTransformationCalculator); #endif // iOS { src1 = helper_.CreateSourceTexture(input); -#if defined(__ANDROID__) +#if defined(TEXTURE_EXTERNAL_OES) if (src1.target() == GL_TEXTURE_EXTERNAL_OES) { if (!ext_rgb_renderer_) { ext_rgb_renderer_ = absl::make_unique(); @@ -417,7 +424,7 @@ REGISTER_CALCULATOR(ImageTransformationCalculator); } renderer = ext_rgb_renderer_.get(); } else // NOLINT(readability/braces) -#endif // __ANDROID__ +#endif // TEXTURE_EXTERNAL_OES { if (!rgb_renderer_) { rgb_renderer_ = absl::make_unique(); @@ -460,7 +467,7 @@ REGISTER_CALCULATOR(ImageTransformationCalculator); auto output = dst.GetFrame(); cc->Outputs().Tag("IMAGE_GPU").Add(output.release(), cc->InputTimestamp()); -#endif // __ANDROID__ || iOS +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } diff --git a/mediapipe/calculators/image/recolor_calculator.cc b/mediapipe/calculators/image/recolor_calculator.cc index b23eda481..fff26b704 100644 --- a/mediapipe/calculators/image/recolor_calculator.cc +++ b/mediapipe/calculators/image/recolor_calculator.cc @@ -21,12 +21,11 @@ #include "mediapipe/framework/port/status.h" #include "mediapipe/util/color.pb.h" -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) #include "mediapipe/gpu/gl_calculator_helper.h" #include "mediapipe/gpu/gl_simple_shaders.h" -#include "mediapipe/gpu/gpu_buffer.h" #include "mediapipe/gpu/shader_util.h" -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU namespace { enum { ATTRIB_VERTEX, ATTRIB_TEXTURE_POSITION, NUM_ATTRIBUTES }; @@ -95,10 +94,10 @@ class RecolorCalculator : public CalculatorBase { mediapipe::RecolorCalculatorOptions::MaskChannel mask_channel_; bool use_gpu_ = false; -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) mediapipe::GlCalculatorHelper gpu_helper_; GLuint program_ = 0; -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU }; REGISTER_CALCULATOR(RecolorCalculator); @@ -107,36 +106,43 @@ REGISTER_CALCULATOR(RecolorCalculator); RET_CHECK(!cc->Inputs().GetTags().empty()); RET_CHECK(!cc->Outputs().GetTags().empty()); -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) + bool use_gpu = false; + +#if !defined(MEDIAPIPE_DISABLE_GPU) if (cc->Inputs().HasTag("IMAGE_GPU")) { cc->Inputs().Tag("IMAGE_GPU").Set(); + use_gpu |= true; } -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU if (cc->Inputs().HasTag("IMAGE")) { cc->Inputs().Tag("IMAGE").Set(); } -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) if (cc->Inputs().HasTag("MASK_GPU")) { cc->Inputs().Tag("MASK_GPU").Set(); + use_gpu |= true; } -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU if (cc->Inputs().HasTag("MASK")) { cc->Inputs().Tag("MASK").Set(); } -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) if (cc->Outputs().HasTag("IMAGE_GPU")) { cc->Outputs().Tag("IMAGE_GPU").Set(); + use_gpu |= true; } -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU if (cc->Outputs().HasTag("IMAGE")) { cc->Outputs().Tag("IMAGE").Set(); } -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) - MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); -#endif // __ANDROID__ or iOS + if (use_gpu) { +#if !defined(MEDIAPIPE_DISABLE_GPU) + MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); +#endif // !MEDIAPIPE_DISABLE_GPU + } return ::mediapipe::OkStatus(); } @@ -146,9 +152,9 @@ REGISTER_CALCULATOR(RecolorCalculator); if (cc->Inputs().HasTag("IMAGE_GPU")) { use_gpu_ = true; -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) MP_RETURN_IF_ERROR(gpu_helper_.Open(cc)); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU } MP_RETURN_IF_ERROR(LoadOptions(cc)); @@ -158,7 +164,7 @@ REGISTER_CALCULATOR(RecolorCalculator); ::mediapipe::Status RecolorCalculator::Process(CalculatorContext* cc) { if (use_gpu_) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) MP_RETURN_IF_ERROR( gpu_helper_.RunInGlContext([this, &cc]() -> ::mediapipe::Status { if (!initialized_) { @@ -168,7 +174,7 @@ REGISTER_CALCULATOR(RecolorCalculator); MP_RETURN_IF_ERROR(RenderGpu(cc)); return ::mediapipe::OkStatus(); })); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU } else { MP_RETURN_IF_ERROR(RenderCpu(cc)); } @@ -176,12 +182,12 @@ REGISTER_CALCULATOR(RecolorCalculator); } ::mediapipe::Status RecolorCalculator::Close(CalculatorContext* cc) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) gpu_helper_.RunInGlContext([this] { if (program_) glDeleteProgram(program_); program_ = 0; }); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } @@ -194,7 +200,7 @@ REGISTER_CALCULATOR(RecolorCalculator); if (cc->Inputs().Tag("MASK_GPU").IsEmpty()) { return ::mediapipe::OkStatus(); } -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) // Get inputs and setup output. const Packet& input_packet = cc->Inputs().Tag("IMAGE_GPU").Value(); const Packet& mask_packet = cc->Inputs().Tag("MASK_GPU").Value(); @@ -233,13 +239,13 @@ REGISTER_CALCULATOR(RecolorCalculator); img_tex.Release(); mask_tex.Release(); dst_tex.Release(); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } void RecolorCalculator::GlRender() { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) static const GLfloat square_vertices[] = { -1.0f, -1.0f, // bottom left 1.0f, -1.0f, // bottom right @@ -287,7 +293,7 @@ void RecolorCalculator::GlRender() { glBindVertexArray(0); glDeleteVertexArrays(1, &vao); glDeleteBuffers(2, vbo); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU } ::mediapipe::Status RecolorCalculator::LoadOptions(CalculatorContext* cc) { @@ -305,7 +311,7 @@ void RecolorCalculator::GlRender() { } ::mediapipe::Status RecolorCalculator::InitGpu(CalculatorContext* cc) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) const GLint attr_location[NUM_ATTRIBUTES] = { ATTRIB_VERTEX, ATTRIB_TEXTURE_POSITION, @@ -374,7 +380,7 @@ void RecolorCalculator::GlRender() { glUniform1i(glGetUniformLocation(program_, "mask"), 2); glUniform3f(glGetUniformLocation(program_, "recolor"), color_[0], color_[1], color_[2]); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } diff --git a/mediapipe/calculators/image/set_alpha_calculator.cc b/mediapipe/calculators/image/set_alpha_calculator.cc index f3f6cedaa..31de1e21a 100644 --- a/mediapipe/calculators/image/set_alpha_calculator.cc +++ b/mediapipe/calculators/image/set_alpha_calculator.cc @@ -25,12 +25,11 @@ #include "mediapipe/framework/port/status.h" #include "mediapipe/framework/port/vector.h" -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) #include "mediapipe/gpu/gl_calculator_helper.h" #include "mediapipe/gpu/gl_simple_shaders.h" -#include "mediapipe/gpu/gpu_buffer.h" #include "mediapipe/gpu/shader_util.h" -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU namespace mediapipe { @@ -107,16 +106,18 @@ class SetAlphaCalculator : public CalculatorBase { bool use_gpu_ = false; bool gpu_initialized_ = false; -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) mediapipe::GlCalculatorHelper gpu_helper_; GLuint program_ = 0; -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU }; REGISTER_CALCULATOR(SetAlphaCalculator); ::mediapipe::Status SetAlphaCalculator::GetContract(CalculatorContract* cc) { CHECK_GE(cc->Inputs().NumEntries(), 1); + bool use_gpu = false; + if (cc->Inputs().HasTag(kInputFrameTag) && cc->Inputs().HasTag(kInputFrameTagGpu)) { return ::mediapipe::InternalError("Cannot have multiple input images."); @@ -127,38 +128,43 @@ REGISTER_CALCULATOR(SetAlphaCalculator); } // Input image to add/edit alpha channel. -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) if (cc->Inputs().HasTag(kInputFrameTagGpu)) { cc->Inputs().Tag(kInputFrameTagGpu).Set(); + use_gpu |= true; } -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU if (cc->Inputs().HasTag(kInputFrameTag)) { cc->Inputs().Tag(kInputFrameTag).Set(); } // Input alpha image mask (optional) -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) if (cc->Inputs().HasTag(kInputAlphaTagGpu)) { cc->Inputs().Tag(kInputAlphaTagGpu).Set(); + use_gpu |= true; } -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU if (cc->Inputs().HasTag(kInputAlphaTag)) { cc->Inputs().Tag(kInputAlphaTag).Set(); } // RGBA output image. -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) if (cc->Outputs().HasTag(kOutputFrameTagGpu)) { cc->Outputs().Tag(kOutputFrameTagGpu).Set(); + use_gpu |= true; } -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU if (cc->Outputs().HasTag(kOutputFrameTag)) { cc->Outputs().Tag(kOutputFrameTag).Set(); } -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) - MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); -#endif // __ANDROID__ or iOS + if (use_gpu) { +#if !defined(MEDIAPIPE_DISABLE_GPU) + MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); +#endif // !MEDIAPIPE_DISABLE_GPU + } return ::mediapipe::OkStatus(); } @@ -170,11 +176,11 @@ REGISTER_CALCULATOR(SetAlphaCalculator); if (cc->Inputs().HasTag(kInputFrameTagGpu) && cc->Outputs().HasTag(kOutputFrameTagGpu)) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) use_gpu_ = true; #else - RET_CHECK_FAIL() << "GPU processing is for Android and iOS only."; -#endif // __ANDROID__ or iOS + RET_CHECK_FAIL() << "GPU processing not enabled."; +#endif // !MEDIAPIPE_DISABLE_GPU } // Get global value from options (-1 if not set). @@ -187,17 +193,17 @@ REGISTER_CALCULATOR(SetAlphaCalculator); RET_CHECK_FAIL() << "Must use either image mask or options alpha value."; if (use_gpu_) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) MP_RETURN_IF_ERROR(gpu_helper_.Open(cc)); #endif - } + } // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } ::mediapipe::Status SetAlphaCalculator::Process(CalculatorContext* cc) { if (use_gpu_) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) MP_RETURN_IF_ERROR( gpu_helper_.RunInGlContext([this, cc]() -> ::mediapipe::Status { if (!gpu_initialized_) { @@ -207,7 +213,7 @@ REGISTER_CALCULATOR(SetAlphaCalculator); MP_RETURN_IF_ERROR(RenderGpu(cc)); return ::mediapipe::OkStatus(); })); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU } else { MP_RETURN_IF_ERROR(RenderCpu(cc)); } @@ -216,12 +222,12 @@ REGISTER_CALCULATOR(SetAlphaCalculator); } ::mediapipe::Status SetAlphaCalculator::Close(CalculatorContext* cc) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) gpu_helper_.RunInGlContext([this] { if (program_) glDeleteProgram(program_); program_ = 0; }); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } @@ -295,7 +301,7 @@ REGISTER_CALCULATOR(SetAlphaCalculator); if (cc->Inputs().Tag(kInputFrameTagGpu).IsEmpty()) { return ::mediapipe::OkStatus(); } -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) // Setup source texture. const auto& input_frame = cc->Inputs().Tag(kInputFrameTagGpu).Get(); @@ -348,13 +354,13 @@ REGISTER_CALCULATOR(SetAlphaCalculator); // Cleanup input_texture.Release(); output_texture.Release(); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } void SetAlphaCalculator::GlRender(CalculatorContext* cc) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) static const GLfloat square_vertices[] = { -1.0f, -1.0f, // bottom left 1.0f, -1.0f, // bottom right @@ -403,11 +409,11 @@ void SetAlphaCalculator::GlRender(CalculatorContext* cc) { glDeleteVertexArrays(1, &vao); glDeleteBuffers(2, vbo); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU } ::mediapipe::Status SetAlphaCalculator::GlSetup(CalculatorContext* cc) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) const GLint attr_location[NUM_ATTRIBUTES] = { ATTRIB_VERTEX, ATTRIB_TEXTURE_POSITION, @@ -460,7 +466,7 @@ void SetAlphaCalculator::GlRender(CalculatorContext* cc) { glUniform1i(glGetUniformLocation(program_, "alpha_mask"), 2); glUniform1f(glGetUniformLocation(program_, "alpha_value"), alpha_value_); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } diff --git a/mediapipe/calculators/tensorflow/BUILD b/mediapipe/calculators/tensorflow/BUILD index 45d6cf965..4231b899e 100644 --- a/mediapipe/calculators/tensorflow/BUILD +++ b/mediapipe/calculators/tensorflow/BUILD @@ -255,6 +255,7 @@ mediapipe_cc_proto_library( cc_deps = [ "//mediapipe/calculators/core:packet_resampler_calculator_cc_proto", "//mediapipe/framework:calculator_cc_proto", + "//mediapipe/util:audio_decoder_cc_proto", ], visibility = ["//visibility:public"], deps = [":unpack_media_sequence_calculator_proto"], @@ -653,6 +654,7 @@ cc_library( "//mediapipe/framework/formats:location", "//mediapipe/framework/port:ret_check", "//mediapipe/framework/port:status", + "//mediapipe/util:audio_decoder_cc_proto", "//mediapipe/util/sequence:media_sequence", "@com_google_absl//absl/strings", "@org_tensorflow//tensorflow/core:protos_all_cc", @@ -769,7 +771,6 @@ cc_test( "//mediapipe/framework/formats:location", "//mediapipe/framework/port:gtest_main", "//mediapipe/framework/port:opencv_imgcodecs", - "//mediapipe/framework/port:status", "//mediapipe/util/sequence:media_sequence", "@com_google_absl//absl/memory", "@com_google_absl//absl/strings", @@ -971,6 +972,7 @@ cc_test( "//mediapipe/framework/formats:location", "//mediapipe/framework/port:gtest_main", "//mediapipe/framework/port:rectangle", + "//mediapipe/util:audio_decoder_cc_proto", "//mediapipe/util/sequence:media_sequence", "@com_google_absl//absl/memory", "@com_google_absl//absl/strings", diff --git a/mediapipe/calculators/tensorflow/pack_media_sequence_calculator.cc b/mediapipe/calculators/tensorflow/pack_media_sequence_calculator.cc index 7780d7850..594e182ec 100644 --- a/mediapipe/calculators/tensorflow/pack_media_sequence_calculator.cc +++ b/mediapipe/calculators/tensorflow/pack_media_sequence_calculator.cc @@ -285,6 +285,10 @@ class PackMediaSequenceCalculator : public CalculatorBase { } ::mediapipe::Status Process(CalculatorContext* cc) override { + int image_height = -1; + int image_width = -1; + // Because the tag order may vary, we need to loop through tags to get + // image information before processing other tag types. for (const auto& tag : cc->Inputs().GetTags()) { if (!cc->Inputs().Tag(tag).IsEmpty()) { features_present_[tag] = true; @@ -306,14 +310,21 @@ class PackMediaSequenceCalculator : public CalculatorBase { return ::mediapipe::InvalidArgumentErrorBuilder(MEDIAPIPE_LOC) << "No encoded image"; } + image_height = image.height(); + image_width = image.width(); mpms::AddImageTimestamp(key, cc->InputTimestamp().Value(), sequence_.get()); mpms::AddImageEncoded(key, image.encoded_image(), sequence_.get()); } + } + for (const auto& tag : cc->Inputs().GetTags()) { + if (!cc->Inputs().Tag(tag).IsEmpty()) { + features_present_[tag] = true; + } if (absl::StartsWith(tag, kKeypointsTag) && !cc->Inputs().Tag(tag).IsEmpty()) { std::string key = ""; - if (tag != kImageTag) { + if (tag != kKeypointsTag) { int tag_length = sizeof(kKeypointsTag) / sizeof(*kKeypointsTag) - 1; if (tag[tag_length] == '_') { key = tag.substr(tag_length + 1); @@ -363,11 +374,20 @@ class PackMediaSequenceCalculator : public CalculatorBase { LocationData::BOUNDING_BOX || detection.location_data().format() == LocationData::RELATIVE_BOUNDING_BOX) { - int height = mpms::GetImageHeight(*sequence_); - int width = mpms::GetImageWidth(*sequence_); + if (mpms::HasImageHeight(*sequence_) && + mpms::HasImageWidth(*sequence_)) { + image_height = mpms::GetImageHeight(*sequence_); + image_width = mpms::GetImageWidth(*sequence_); + } + if (image_height == -1 || image_width == -1) { + return ::mediapipe::InvalidArgumentErrorBuilder(MEDIAPIPE_LOC) + << "Images must be provided with bounding boxes or the " + "image " + << "height and width must already be in the example."; + } Location relative_bbox = Location::CreateRelativeBBoxLocation( Location(detection.location_data()) - .ConvertToRelativeBBox(width, height)); + .ConvertToRelativeBBox(image_width, image_height)); predicted_locations.push_back(relative_bbox); if (detection.label_size() > 0) { predicted_class_strings.push_back(detection.label(0)); diff --git a/mediapipe/calculators/tensorflow/pack_media_sequence_calculator_test.cc b/mediapipe/calculators/tensorflow/pack_media_sequence_calculator_test.cc index 19b302e13..df43a921f 100644 --- a/mediapipe/calculators/tensorflow/pack_media_sequence_calculator_test.cc +++ b/mediapipe/calculators/tensorflow/pack_media_sequence_calculator_test.cc @@ -357,6 +357,148 @@ TEST_F(PackMediaSequenceCalculatorTest, PacksTwoBBoxDetections) { } } +TEST_F(PackMediaSequenceCalculatorTest, PacksBBoxWithoutImageDims) { + SetUpCalculator({"BBOX_PREDICTED:detections"}, {}, false, true); + auto input_sequence = ::absl::make_unique(); + std::string test_video_id = "test_video_id"; + mpms::SetClipMediaId(test_video_id, input_sequence.get()); + int height = 480; + int width = 640; + int num_vectors = 2; + for (int i = 0; i < num_vectors; ++i) { + auto detections = ::absl::make_unique<::std::vector>(); + Detection detection; + detection.add_label("absolute bbox"); + detection.add_label_id(0); + detection.add_score(0.5); + Location::CreateBBoxLocation(0, height / 2, width / 2, height / 2) + .ConvertToProto(detection.mutable_location_data()); + detections->push_back(detection); + + detection = Detection(); + detection.add_label("relative bbox"); + detection.add_label_id(1); + detection.add_score(0.75); + Location::CreateRelativeBBoxLocation(0, 0.5, 0.5, 0.5) + .ConvertToProto(detection.mutable_location_data()); + detections->push_back(detection); + + // The mask detection should be ignored in the output. + detection = Detection(); + detection.add_label("mask"); + detection.add_score(1.0); + cv::Mat image(2, 3, CV_8UC1, cv::Scalar(0)); + Location::CreateCvMaskLocation(image).ConvertToProto( + detection.mutable_location_data()); + detections->push_back(detection); + + runner_->MutableInputs() + ->Tag("BBOX_PREDICTED") + .packets.push_back(Adopt(detections.release()).At(Timestamp(i))); + } + + runner_->MutableSidePackets()->Tag("SEQUENCE_EXAMPLE") = + Adopt(input_sequence.release()); + + auto status = runner_->Run(); + EXPECT_EQ(::mediapipe::StatusCode::kInvalidArgument, status.code()); +} + +TEST_F(PackMediaSequenceCalculatorTest, PacksBBoxWithImages) { + SetUpCalculator({"BBOX_PREDICTED:detections", "IMAGE:images"}, {}, false, + true); + auto input_sequence = ::absl::make_unique(); + std::string test_video_id = "test_video_id"; + mpms::SetClipMediaId(test_video_id, input_sequence.get()); + int height = 480; + int width = 640; + int num_vectors = 2; + for (int i = 0; i < num_vectors; ++i) { + auto detections = ::absl::make_unique<::std::vector>(); + Detection detection; + detection.add_label("absolute bbox"); + detection.add_label_id(0); + detection.add_score(0.5); + Location::CreateBBoxLocation(0, height / 2, width / 2, height / 2) + .ConvertToProto(detection.mutable_location_data()); + detections->push_back(detection); + + detection = Detection(); + detection.add_label("relative bbox"); + detection.add_label_id(1); + detection.add_score(0.75); + Location::CreateRelativeBBoxLocation(0, 0.5, 0.5, 0.5) + .ConvertToProto(detection.mutable_location_data()); + detections->push_back(detection); + + // The mask detection should be ignored in the output. + detection = Detection(); + detection.add_label("mask"); + detection.add_score(1.0); + cv::Mat image(2, 3, CV_8UC1, cv::Scalar(0)); + Location::CreateCvMaskLocation(image).ConvertToProto( + detection.mutable_location_data()); + detections->push_back(detection); + + runner_->MutableInputs() + ->Tag("BBOX_PREDICTED") + .packets.push_back(Adopt(detections.release()).At(Timestamp(i))); + } + cv::Mat image(height, width, CV_8UC3, cv::Scalar(0, 0, 255)); + std::vector bytes; + ASSERT_TRUE(cv::imencode(".jpg", image, bytes, {80})); + std::string test_image_string(bytes.begin(), bytes.end()); + OpenCvImageEncoderCalculatorResults encoded_image; + encoded_image.set_encoded_image(test_image_string); + encoded_image.set_width(width); + encoded_image.set_height(height); + + int num_images = 2; + for (int i = 0; i < num_images; ++i) { + auto image_ptr = + ::absl::make_unique(encoded_image); + runner_->MutableInputs()->Tag("IMAGE").packets.push_back( + Adopt(image_ptr.release()).At(Timestamp(i))); + } + runner_->MutableSidePackets()->Tag("SEQUENCE_EXAMPLE") = + Adopt(input_sequence.release()); + + MP_ASSERT_OK(runner_->Run()); + + const std::vector& output_packets = + runner_->Outputs().Tag("SEQUENCE_EXAMPLE").packets; + ASSERT_EQ(1, output_packets.size()); + const tf::SequenceExample& output_sequence = + output_packets[0].Get(); + + ASSERT_EQ(test_video_id, mpms::GetClipMediaId(output_sequence)); + ASSERT_EQ(height, mpms::GetImageHeight(output_sequence)); + ASSERT_EQ(width, mpms::GetImageWidth(output_sequence)); + ASSERT_EQ(num_vectors, mpms::GetPredictedBBoxSize(output_sequence)); + ASSERT_EQ(num_vectors, mpms::GetPredictedBBoxTimestampSize(output_sequence)); + ASSERT_EQ(0, mpms::GetClassSegmentationEncodedSize(output_sequence)); + ASSERT_EQ(0, mpms::GetClassSegmentationTimestampSize(output_sequence)); + for (int i = 0; i < num_vectors; ++i) { + ASSERT_EQ(i, mpms::GetPredictedBBoxTimestampAt(output_sequence, i)); + auto bboxes = mpms::GetPredictedBBoxAt(output_sequence, i); + ASSERT_EQ(2, bboxes.size()); + for (int j = 0; j < bboxes.size(); ++j) { + auto rect = bboxes[j].GetRelativeBBox(); + ASSERT_NEAR(0, rect.xmin(), 0.001); + ASSERT_NEAR(0.5, rect.ymin(), 0.001); + ASSERT_NEAR(0.5, rect.xmax(), 0.001); + ASSERT_NEAR(1.0, rect.ymax(), 0.001); + } + auto class_strings = + mpms::GetPredictedBBoxLabelStringAt(output_sequence, i); + ASSERT_EQ("absolute bbox", class_strings[0]); + ASSERT_EQ("relative bbox", class_strings[1]); + auto class_indices = mpms::GetPredictedBBoxLabelIndexAt(output_sequence, i); + ASSERT_EQ(0, class_indices[0]); + ASSERT_EQ(1, class_indices[1]); + } +} + TEST_F(PackMediaSequenceCalculatorTest, PacksTwoKeypoints) { SetUpCalculator({"KEYPOINTS_TEST:keypoints"}, {}, false, true); auto input_sequence = ::absl::make_unique(); diff --git a/mediapipe/calculators/tensorflow/unpack_media_sequence_calculator.cc b/mediapipe/calculators/tensorflow/unpack_media_sequence_calculator.cc index 51493d7b6..a92b48d30 100644 --- a/mediapipe/calculators/tensorflow/unpack_media_sequence_calculator.cc +++ b/mediapipe/calculators/tensorflow/unpack_media_sequence_calculator.cc @@ -19,6 +19,7 @@ #include "mediapipe/framework/formats/location.h" #include "mediapipe/framework/port/ret_check.h" #include "mediapipe/framework/port/status.h" +#include "mediapipe/util/audio_decoder.pb.h" #include "mediapipe/util/sequence/media_sequence.h" #include "tensorflow/core/example/example.pb.h" #include "tensorflow/core/example/feature.pb.h" @@ -37,6 +38,7 @@ const char kDatasetRootDirTag[] = "DATASET_ROOT"; const char kDataPath[] = "DATA_PATH"; const char kPacketResamplerOptions[] = "RESAMPLER_OPTIONS"; const char kImagesFrameRateTag[] = "IMAGE_FRAME_RATE"; +const char kAudioDecoderOptions[] = "AUDIO_DECODER_OPTIONS"; namespace tf = ::tensorflow; namespace mpms = ::mediapipe::mediasequence; @@ -126,6 +128,11 @@ class UnpackMediaSequenceCalculator : public CalculatorBase { if (cc->OutputSidePackets().HasTag(kDataPath)) { cc->OutputSidePackets().Tag(kDataPath).Set(); } + if (cc->OutputSidePackets().HasTag(kAudioDecoderOptions)) { + cc->OutputSidePackets() + .Tag(kAudioDecoderOptions) + .Set(); + } if (cc->OutputSidePackets().HasTag(kImagesFrameRateTag)) { cc->OutputSidePackets().Tag(kImagesFrameRateTag).Set(); } @@ -136,10 +143,11 @@ class UnpackMediaSequenceCalculator : public CalculatorBase { } if ((options.has_padding_before_label() || options.has_padding_after_label()) && - !(cc->OutputSidePackets().HasTag(kPacketResamplerOptions))) { + !(cc->OutputSidePackets().HasTag(kAudioDecoderOptions) || + cc->OutputSidePackets().HasTag(kPacketResamplerOptions))) { return ::mediapipe::InvalidArgumentErrorBuilder(MEDIAPIPE_LOC) - << "If specifying padding, must output " - << kPacketResamplerOptions; + << "If specifying padding, must output " << kPacketResamplerOptions + << "or" << kAudioDecoderOptions; } // Optional streams. @@ -260,7 +268,8 @@ class UnpackMediaSequenceCalculator : public CalculatorBase { // Set the start and end of the clip in the appropriate options protos. double start_time = 0; double end_time = 0; - if (cc->OutputSidePackets().HasTag(kPacketResamplerOptions)) { + if (cc->OutputSidePackets().HasTag(kAudioDecoderOptions) || + cc->OutputSidePackets().HasTag(kPacketResamplerOptions)) { if (mpms::HasClipStartTimestamp(sequence)) { start_time = Timestamp(mpms::GetClipStartTimestamp(sequence)).Seconds() - @@ -271,6 +280,27 @@ class UnpackMediaSequenceCalculator : public CalculatorBase { options.padding_after_label(); } } + if (cc->OutputSidePackets().HasTag(kAudioDecoderOptions)) { + auto audio_decoder_options = absl::make_unique( + options.base_audio_decoder_options()); + if (mpms::HasClipStartTimestamp(sequence)) { + if (options.force_decoding_from_start_of_media()) { + audio_decoder_options->set_start_time(0); + } else { + audio_decoder_options->set_start_time( + start_time - options.extra_padding_from_media_decoder()); + } + } + if (mpms::HasClipEndTimestamp(sequence)) { + audio_decoder_options->set_end_time( + end_time + options.extra_padding_from_media_decoder()); + } + LOG(INFO) << "Created AudioDecoderOptions:\n" + << audio_decoder_options->DebugString(); + cc->OutputSidePackets() + .Tag(kAudioDecoderOptions) + .Set(Adopt(audio_decoder_options.release())); + } if (cc->OutputSidePackets().HasTag(kPacketResamplerOptions)) { auto resampler_options = absl::make_unique(); *(resampler_options->MutableExtension( diff --git a/mediapipe/calculators/tensorflow/unpack_media_sequence_calculator.proto b/mediapipe/calculators/tensorflow/unpack_media_sequence_calculator.proto index e6e839645..51cc870c7 100644 --- a/mediapipe/calculators/tensorflow/unpack_media_sequence_calculator.proto +++ b/mediapipe/calculators/tensorflow/unpack_media_sequence_calculator.proto @@ -18,6 +18,7 @@ package mediapipe; import "mediapipe/calculators/core/packet_resampler_calculator.proto"; import "mediapipe/framework/calculator.proto"; +import "mediapipe/util/audio_decoder.proto"; message UnpackMediaSequenceCalculatorOptions { extend mediapipe.CalculatorOptions { @@ -49,4 +50,10 @@ message UnpackMediaSequenceCalculatorOptions { // parameters for the MediaDecoderCalculator. End time parameters are still // respected. optional bool force_decoding_from_start_of_media = 7; + + // Stores the audio decoder settings for the graph. (e.g. which audio + // stream to pull from the video.) The sequence's metadata overrides + // the clip start and end times and outputs these for the + // AudioDecoderCalculator to consume. + optional AudioDecoderOptions base_audio_decoder_options = 9; } diff --git a/mediapipe/calculators/tensorflow/unpack_media_sequence_calculator_test.cc b/mediapipe/calculators/tensorflow/unpack_media_sequence_calculator_test.cc index 36958ef8f..185e2e186 100644 --- a/mediapipe/calculators/tensorflow/unpack_media_sequence_calculator_test.cc +++ b/mediapipe/calculators/tensorflow/unpack_media_sequence_calculator_test.cc @@ -23,6 +23,7 @@ #include "mediapipe/framework/port/gtest.h" #include "mediapipe/framework/port/rectangle.h" #include "mediapipe/framework/port/status_matchers.h" +#include "mediapipe/util/audio_decoder.pb.h" #include "mediapipe/util/sequence/media_sequence.h" #include "tensorflow/core/example/example.pb.h" @@ -459,6 +460,62 @@ TEST_F(UnpackMediaSequenceCalculatorTest, GetDatasetFromExample) { data_path_); } +TEST_F(UnpackMediaSequenceCalculatorTest, GetAudioDecoderOptions) { + CalculatorOptions options; + options.MutableExtension(UnpackMediaSequenceCalculatorOptions::ext) + ->set_padding_before_label(1); + options.MutableExtension(UnpackMediaSequenceCalculatorOptions::ext) + ->set_padding_after_label(2); + SetUpCalculator({}, {"AUDIO_DECODER_OPTIONS:audio_decoder_options"}, {}, + &options); + runner_->MutableSidePackets()->Tag("SEQUENCE_EXAMPLE") = + Adopt(sequence_.release()); + MP_ASSERT_OK(runner_->Run()); + + MP_EXPECT_OK(runner_->OutputSidePackets() + .Tag("AUDIO_DECODER_OPTIONS") + .ValidateAsType()); + EXPECT_NEAR(runner_->OutputSidePackets() + .Tag("AUDIO_DECODER_OPTIONS") + .Get() + .start_time(), + 2.0, 1e-5); + EXPECT_NEAR(runner_->OutputSidePackets() + .Tag("AUDIO_DECODER_OPTIONS") + .Get() + .end_time(), + 7.0, 1e-5); +} + +TEST_F(UnpackMediaSequenceCalculatorTest, GetAudioDecoderOptionsOverride) { + CalculatorOptions options; + options.MutableExtension(UnpackMediaSequenceCalculatorOptions::ext) + ->set_padding_before_label(1); + options.MutableExtension(UnpackMediaSequenceCalculatorOptions::ext) + ->set_padding_after_label(2); + options.MutableExtension(UnpackMediaSequenceCalculatorOptions::ext) + ->set_force_decoding_from_start_of_media(true); + SetUpCalculator({}, {"AUDIO_DECODER_OPTIONS:audio_decoder_options"}, {}, + &options); + runner_->MutableSidePackets()->Tag("SEQUENCE_EXAMPLE") = + Adopt(sequence_.release()); + MP_ASSERT_OK(runner_->Run()); + + MP_EXPECT_OK(runner_->OutputSidePackets() + .Tag("AUDIO_DECODER_OPTIONS") + .ValidateAsType()); + EXPECT_NEAR(runner_->OutputSidePackets() + .Tag("AUDIO_DECODER_OPTIONS") + .Get() + .start_time(), + 0.0, 1e-5); + EXPECT_NEAR(runner_->OutputSidePackets() + .Tag("AUDIO_DECODER_OPTIONS") + .Get() + .end_time(), + 7.0, 1e-5); +} + TEST_F(UnpackMediaSequenceCalculatorTest, GetPacketResamplingOptions) { // TODO: Suport proto3 proto.Any in CalculatorOptions. // TODO: Avoid proto2 extensions in "RESAMPLER_OPTIONS". diff --git a/mediapipe/calculators/tflite/BUILD b/mediapipe/calculators/tflite/BUILD index 93f08edc5..a0b1fc0b6 100644 --- a/mediapipe/calculators/tflite/BUILD +++ b/mediapipe/calculators/tflite/BUILD @@ -195,6 +195,12 @@ cc_test( ], ) +cc_library( + name = "util", + hdrs = ["util.h"], + alwayslink = 1, +) + cc_library( name = "tflite_inference_calculator", srcs = ["tflite_inference_calculator.cc"], @@ -214,6 +220,7 @@ cc_library( }), visibility = ["//visibility:public"], deps = [ + ":util", ":tflite_inference_calculator_cc_proto", "//mediapipe/framework:calculator_framework", "//mediapipe/util:resource_util", @@ -222,20 +229,25 @@ cc_library( "//mediapipe/framework/stream_handler:fixed_size_input_stream_handler", "//mediapipe/framework/port:ret_check", ] + select({ - "//mediapipe:android": [ + "//mediapipe/gpu:disable_gpu": [], + "//mediapipe:ios": [ + "//mediapipe/gpu:MPPMetalHelper", + "//mediapipe/gpu:MPPMetalUtil", + "//mediapipe/gpu:gpu_buffer", + "//mediapipe/objc:mediapipe_framework_ios", + "@org_tensorflow//tensorflow/lite/delegates/gpu/common:shape", + "@org_tensorflow//tensorflow/lite/delegates/gpu/metal:buffer_convert", + "@org_tensorflow//tensorflow/lite/delegates/gpu:metal_delegate", + ], + "//conditions:default": [ "//mediapipe/gpu:gl_calculator_helper", "//mediapipe/gpu:gpu_buffer", + "@org_tensorflow//tensorflow/lite/delegates/gpu/common:shape", "@org_tensorflow//tensorflow/lite/delegates/gpu:gl_delegate", "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_buffer", "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_program", "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_shader", ], - "//mediapipe:ios": [ - "//mediapipe/gpu:MPPMetalHelper", - "//mediapipe/objc:mediapipe_framework_ios", - "@org_tensorflow//tensorflow/lite/delegates/gpu:metal_delegate", - ], - "//conditions:default": [], }), alwayslink = 1, ) @@ -259,33 +271,33 @@ cc_library( }), visibility = ["//visibility:public"], deps = [ + ":util", ":tflite_converter_calculator_cc_proto", "//mediapipe/util:resource_util", "//mediapipe/framework:calculator_framework", "//mediapipe/framework/formats:image_frame", "//mediapipe/framework/formats:matrix", "//mediapipe/framework/stream_handler:fixed_size_input_stream_handler", - "//mediapipe/framework/tool:status_util", - "//mediapipe/framework/port:status", "//mediapipe/framework/port:ret_check", "@org_tensorflow//tensorflow/lite:framework", "@org_tensorflow//tensorflow/lite/kernels:builtin_ops", ] + select({ - "//mediapipe:android": [ - "//mediapipe/gpu:gl_calculator_helper", - "//mediapipe/gpu:gpu_buffer", - "@org_tensorflow//tensorflow/lite/delegates/gpu:gl_delegate", - "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_buffer", - "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_program", - "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_shader", - ], + "//mediapipe/gpu:disable_gpu": [], "//mediapipe:ios": [ + "//mediapipe/gpu:MPPMetalUtil", "//mediapipe/gpu:gpu_buffer", "//mediapipe/gpu:MPPMetalHelper", "//mediapipe/objc:mediapipe_framework_ios", "@org_tensorflow//tensorflow/lite/delegates/gpu:metal_delegate", ], - "//conditions:default": [], + "//conditions:default": [ + "//mediapipe/gpu:gpu_buffer", + "//mediapipe/gpu:gl_calculator_helper", + "@org_tensorflow//tensorflow/lite/delegates/gpu:gl_delegate", + "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_buffer", + "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_program", + "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_shader", + ], }), alwayslink = 1, ) @@ -295,6 +307,7 @@ cc_library( srcs = ["tflite_tensors_to_segmentation_calculator.cc"], visibility = ["//visibility:public"], deps = [ + ":util", ":tflite_tensors_to_segmentation_calculator_cc_proto", "@com_google_absl//absl/strings:str_format", "@com_google_absl//absl/types:span", @@ -308,7 +321,9 @@ cc_library( "//mediapipe/util:resource_util", "@org_tensorflow//tensorflow/lite:framework", ] + select({ - "//mediapipe:android": [ + "//mediapipe/gpu:disable_gpu": [], + "//mediapipe:ios": [], + "//conditions:default": [ "//mediapipe/gpu:gl_calculator_helper", "//mediapipe/gpu:gl_simple_shaders", "//mediapipe/gpu:gpu_buffer", @@ -319,7 +334,6 @@ cc_library( "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_shader", "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_texture", ], - "//conditions:default": [], }), alwayslink = 1, ) @@ -346,8 +360,23 @@ cc_test( cc_library( name = "tflite_tensors_to_detections_calculator", srcs = ["tflite_tensors_to_detections_calculator.cc"], + copts = select({ + "//mediapipe:ios": [ + "-x objective-c++", + "-fobjc-arc", # enable reference-counting + ], + "//conditions:default": [], + }), + linkopts = select({ + "//mediapipe:ios": [ + "-framework CoreVideo", + "-framework MetalKit", + ], + "//conditions:default": [], + }), visibility = ["//visibility:public"], deps = [ + ":util", ":tflite_tensors_to_detections_calculator_cc_proto", "//mediapipe/framework/formats:detection_cc_proto", "@com_google_absl//absl/strings:str_format", @@ -359,14 +388,21 @@ cc_library( "//mediapipe/framework/port:ret_check", "@org_tensorflow//tensorflow/lite:framework", ] + select({ - "//mediapipe:android": [ + "//mediapipe/gpu:disable_gpu": [], + "//mediapipe:ios": [ + "//mediapipe/gpu:MPPMetalUtil", + "//mediapipe/gpu:gpu_buffer", + "//mediapipe/gpu:MPPMetalHelper", + "//mediapipe/objc:mediapipe_framework_ios", + "@org_tensorflow//tensorflow/lite/delegates/gpu:metal_delegate", + ], + "//conditions:default": [ "//mediapipe/gpu:gl_calculator_helper", "@org_tensorflow//tensorflow/lite/delegates/gpu:gl_delegate", "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_buffer", "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_program", "@org_tensorflow//tensorflow/lite/delegates/gpu/gl:gl_shader", ], - "//conditions:default": [], }), alwayslink = 1, ) diff --git a/mediapipe/calculators/tflite/tflite_converter_calculator.cc b/mediapipe/calculators/tflite/tflite_converter_calculator.cc index 37952008b..598ae4965 100644 --- a/mediapipe/calculators/tflite/tflite_converter_calculator.cc +++ b/mediapipe/calculators/tflite/tflite_converter_calculator.cc @@ -16,23 +16,23 @@ #include #include "mediapipe/calculators/tflite/tflite_converter_calculator.pb.h" +#include "mediapipe/calculators/tflite/util.h" #include "mediapipe/framework/calculator_framework.h" #include "mediapipe/framework/formats/image_frame.h" #include "mediapipe/framework/formats/matrix.h" -#include "mediapipe/framework/port/canonical_errors.h" #include "mediapipe/framework/port/ret_check.h" #include "mediapipe/util/resource_util.h" #include "tensorflow/lite/error_reporter.h" #include "tensorflow/lite/interpreter.h" -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) #include "mediapipe/gpu/gl_calculator_helper.h" #include "mediapipe/gpu/gpu_buffer.h" #include "tensorflow/lite/delegates/gpu/gl/gl_buffer.h" #include "tensorflow/lite/delegates/gpu/gl/gl_program.h" #include "tensorflow/lite/delegates/gpu/gl/gl_shader.h" #include "tensorflow/lite/delegates/gpu/gl_delegate.h" -#endif // __ANDROID__ +#endif // !MEDIAPIPE_DISABLE_GPU #if defined(__APPLE__) && !TARGET_OS_OSX // iOS #import @@ -40,11 +40,12 @@ #import #import "mediapipe/gpu/MPPMetalHelper.h" +#include "mediapipe/gpu/MPPMetalUtil.h" #include "mediapipe/gpu/gpu_buffer.h" #include "tensorflow/lite/delegates/gpu/metal_delegate.h" #endif // iOS -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) typedef ::tflite::gpu::gl::GlBuffer GpuTensor; #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS typedef id GpuTensor; @@ -66,26 +67,27 @@ typedef Eigen::Matrix namespace mediapipe { -#if defined(__ANDROID__) -using ::tflite::gpu::gl::GlBuffer; +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +using ::tflite::gpu::gl::CreateReadWriteShaderStorageBuffer; using ::tflite::gpu::gl::GlProgram; using ::tflite::gpu::gl::GlShader; struct GPUData { int elements = 1; - GlBuffer buffer; + GpuTensor buffer; GlShader shader; GlProgram program; }; #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS struct GPUData { int elements = 1; - id buffer; + GpuTensor buffer; id pipeline_state; }; #endif // Calculator for normalizing and converting an ImageFrame or Matrix -// into a TfLiteTensor (float 32) or a GpuBuffer to a tflite::gpu::GlBuffer. +// into a TfLiteTensor (float 32) or a GpuBuffer to a tflite::gpu::GlBuffer +// or MTLBuffer. // // This calculator is designed to be used with the TfLiteInferenceCalcualtor, // as a pre-processing step for calculator inputs. @@ -102,7 +104,7 @@ struct GPUData { // Output: // One of the following tags: // TENSORS - Vector of TfLiteTensor of type kTfLiteFloat32, or kTfLiteUint8. -// TENSORS_GPU - vector of GlBuffer. +// TENSORS_GPU - vector of GlBuffer or MTLBuffer. // // Example use: // node { @@ -144,7 +146,7 @@ class TfLiteConverterCalculator : public CalculatorBase { std::unique_ptr interpreter_ = nullptr; -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) mediapipe::GlCalculatorHelper gpu_helper_; std::unique_ptr gpu_data_out_; #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS @@ -175,25 +177,33 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); RET_CHECK(cc->Outputs().HasTag("TENSORS") ^ cc->Outputs().HasTag("TENSORS_GPU")); + bool use_gpu = false; + if (cc->Inputs().HasTag("IMAGE")) cc->Inputs().Tag("IMAGE").Set(); if (cc->Inputs().HasTag("MATRIX")) cc->Inputs().Tag("MATRIX").Set(); -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) - if (cc->Inputs().HasTag("IMAGE_GPU")) +#if !defined(MEDIAPIPE_DISABLE_GPU) + if (cc->Inputs().HasTag("IMAGE_GPU")) { cc->Inputs().Tag("IMAGE_GPU").Set(); -#endif + use_gpu |= true; + } +#endif // !MEDIAPIPE_DISABLE_GPU if (cc->Outputs().HasTag("TENSORS")) cc->Outputs().Tag("TENSORS").Set>(); -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) - if (cc->Outputs().HasTag("TENSORS_GPU")) +#if !defined(MEDIAPIPE_DISABLE_GPU) + if (cc->Outputs().HasTag("TENSORS_GPU")) { cc->Outputs().Tag("TENSORS_GPU").Set>(); -#endif + use_gpu |= true; + } +#endif // !MEDIAPIPE_DISABLE_GPU -#if defined(__ANDROID__) - MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); + if (use_gpu) { +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) + MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS - MP_RETURN_IF_ERROR([MPPMetalHelper updateContract:cc]); + MP_RETURN_IF_ERROR([MPPMetalHelper updateContract:cc]); #endif + } // Assign this calculator's default InputStreamHandler. cc->SetInputStreamHandler("FixedSizeInputStreamHandler"); @@ -208,10 +218,10 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); if (cc->Inputs().HasTag("IMAGE_GPU") || cc->Outputs().HasTag("IMAGE_OUT_GPU")) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) use_gpu_ = true; #else - RET_CHECK_FAIL() << "GPU processing is for Android and iOS only."; + RET_CHECK_FAIL() << "GPU processing not enabled."; #endif } @@ -221,7 +231,7 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); cc->Outputs().HasTag("TENSORS_GPU")); // Cannot use quantization. use_quantized_tensors_ = false; -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) MP_RETURN_IF_ERROR(gpu_helper_.Open(cc)); #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS gpu_helper_ = [[MPPMetalHelper alloc] initWithCalculatorContext:cc]; @@ -238,6 +248,7 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); ::mediapipe::Status TfLiteConverterCalculator::Process(CalculatorContext* cc) { if (use_gpu_) { + // GpuBuffer to tflite::gpu::GlBuffer conversion. if (!initialized_) { MP_RETURN_IF_ERROR(InitGpu(cc)); initialized_ = true; @@ -253,7 +264,7 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); } ::mediapipe::Status TfLiteConverterCalculator::Close(CalculatorContext* cc) { -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) gpu_helper_.RunInGlContext([this] { gpu_data_out_.reset(); }); #endif #if defined(__APPLE__) && !TARGET_OS_OSX // iOS @@ -372,7 +383,7 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); ::mediapipe::Status TfLiteConverterCalculator::ProcessGPU( CalculatorContext* cc) { -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) // GpuBuffer to tflite::gpu::GlBuffer conversion. const auto& input = cc->Inputs().Tag("IMAGE_GPU").Get(); MP_RETURN_IF_ERROR( @@ -381,17 +392,11 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); auto src = gpu_helper_.CreateSourceTexture(input); glActiveTexture(GL_TEXTURE0 + 0); glBindTexture(GL_TEXTURE_2D, src.name()); - auto status = gpu_data_out_->buffer.BindToIndex(1); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } + RET_CHECK_CALL(gpu_data_out_->buffer.BindToIndex(1)); const tflite::gpu::uint3 workgroups = { NumGroups(input.width(), kWorkgroupSize), NumGroups(input.height(), kWorkgroupSize), 1}; - status = gpu_data_out_->program.Dispatch(workgroups); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } + RET_CHECK_CALL(gpu_data_out_->program.Dispatch(workgroups)); glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); glBindTexture(GL_TEXTURE_2D, 0); src.Release(); @@ -400,17 +405,17 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); // Copy into outputs. auto output_tensors = absl::make_unique>(); - output_tensors->resize(1); - { - GlBuffer& tensor = output_tensors->at(0); - using ::tflite::gpu::gl::CreateReadWriteShaderStorageBuffer; - auto status = CreateReadWriteShaderStorageBuffer( - gpu_data_out_->elements, &tensor); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } - tflite::gpu::gl::CopyBuffer(gpu_data_out_->buffer, tensor); - } + MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext( + [this, &output_tensors]() -> ::mediapipe::Status { + output_tensors->resize(1); + { + GpuTensor& tensor = output_tensors->at(0); + RET_CHECK_CALL(CreateReadWriteShaderStorageBuffer( + gpu_data_out_->elements, &tensor)); + RET_CHECK_CALL(CopyBuffer(gpu_data_out_->buffer, tensor)); + } + return ::mediapipe::OkStatus(); + })); cc->Outputs() .Tag("TENSORS_GPU") .Add(output_tensors.release(), cc->InputTimestamp()); @@ -438,66 +443,60 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); } // Copy into outputs. + // TODO Avoid this copy. auto output_tensors = absl::make_unique>(); + output_tensors->resize(1); { id device = gpu_helper_.mtlDevice; - id command_buffer = [gpu_helper_ commandBuffer]; - command_buffer.label = @"TfLiteConverterCalculatorCopy"; - id tensor = + output_tensors->at(0) = [device newBufferWithLength:gpu_data_out_->elements * sizeof(float) options:MTLResourceStorageModeShared]; - id blit_command = - [command_buffer blitCommandEncoder]; - [blit_command copyFromBuffer:gpu_data_out_->buffer - sourceOffset:0 - toBuffer:tensor - destinationOffset:0 - size:gpu_data_out_->elements * sizeof(float)]; - [blit_command endEncoding]; - [command_buffer commit]; - [command_buffer waitUntilCompleted]; - - output_tensors->push_back(tensor); + [MPPMetalUtil blitMetalBufferTo:output_tensors->at(0) + from:gpu_data_out_->buffer + blocking:true + commandBuffer:[gpu_helper_ commandBuffer]]; } cc->Outputs() .Tag("TENSORS_GPU") .Add(output_tensors.release(), cc->InputTimestamp()); #else - RET_CHECK_FAIL() << "GPU processing is for Android and iOS only."; + RET_CHECK_FAIL() << "GPU processing is not enabled."; #endif return ::mediapipe::OkStatus(); } ::mediapipe::Status TfLiteConverterCalculator::InitGpu(CalculatorContext* cc) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) - // Configure inputs. +#if !defined(MEDIAPIPE_DISABLE_GPU) + // Get input image sizes. const auto& input = cc->Inputs().Tag("IMAGE_GPU").Get(); mediapipe::ImageFormat::Format format = mediapipe::ImageFormatForGpuBufferFormat(input.format()); gpu_data_out_ = absl::make_unique(); gpu_data_out_->elements = input.height() * input.width() * max_num_channels_; const bool include_alpha = (max_num_channels_ == 4); - if (!(format == mediapipe::ImageFormat::SRGB || + const bool single_channel = (max_num_channels_ == 1); + if (!(format == mediapipe::ImageFormat::GRAY8 || + format == mediapipe::ImageFormat::SRGB || format == mediapipe::ImageFormat::SRGBA)) RET_CHECK_FAIL() << "Unsupported GPU input format."; if (include_alpha && (format != mediapipe::ImageFormat::SRGBA)) RET_CHECK_FAIL() << "Num input channels is less than desired output."; -#endif +#endif // !MEDIAPIPE_DISABLE_GPU -#if defined(__ANDROID__) - // Device memory. - auto status = ::tflite::gpu::gl::CreateReadWriteShaderStorageBuffer( - gpu_data_out_->elements, &gpu_data_out_->buffer); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) + MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext( + [this, &include_alpha, &input, &single_channel]() -> ::mediapipe::Status { + // Device memory. + RET_CHECK_CALL( + ::tflite::gpu::gl::CreateReadWriteShaderStorageBuffer( + gpu_data_out_->elements, &gpu_data_out_->buffer)); - // Shader to convert GL Texture to Shader Storage Buffer Object (SSBO), - // with normalization to either: [0,1] or [-1,1]. - const std::string shader_source = absl::Substitute( - R"( #version 310 es + // Shader to convert GL Texture to Shader Storage Buffer Object (SSBO), + // with normalization to either: [0,1] or [-1,1]. + const std::string shader_source = absl::Substitute( + R"( #version 310 es layout(local_size_x = $0, local_size_y = $0) in; layout(binding = 0) uniform sampler2D input_texture; layout(std430, binding = 1) buffer Output {float elements[];} output_data; @@ -505,33 +504,31 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); void main() { ivec2 gid = ivec2(gl_GlobalInvocationID.xy); if (gid.x >= width_height.x || gid.y >= width_height.y) return; - $5 // pixel fetch + vec4 pixel = texelFetch(input_texture, gid, 0); $3 // normalize [-1,1] int linear_index = $7 * ($4 * width_height.x + gid.x); - output_data.elements[linear_index + 0] = pixel.x; - output_data.elements[linear_index + 1] = pixel.y; - output_data.elements[linear_index + 2] = pixel.z; + output_data.elements[linear_index + 0] = pixel.x; // r channel + $5 // g & b channels $6 // alpha channel })", - /*$0=*/kWorkgroupSize, /*$1=*/input.width(), /*$2=*/input.height(), - /*$3=*/zero_center_ ? "pixel = (pixel - 0.5) * 2.0;" : "", - /*$4=*/flip_vertically_ ? "(width_height.y - 1 - gid.y)" : "gid.y", - /*$5=*/ - include_alpha ? "vec4 pixel = texelFetch(input_texture, gid, 0);" - : "vec3 pixel = texelFetch(input_texture, gid, 0).xyz;", - /*$6=*/ - include_alpha ? "output_data.elements[linear_index + 3] = pixel.w;" : "", - /*$7=*/include_alpha ? 4 : 3); - status = GlShader::CompileShader(GL_COMPUTE_SHADER, shader_source, - &gpu_data_out_->shader); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } - status = GlProgram::CreateWithShader(gpu_data_out_->shader, - &gpu_data_out_->program); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } + /*$0=*/kWorkgroupSize, /*$1=*/input.width(), /*$2=*/input.height(), + /*$3=*/zero_center_ ? "pixel = (pixel - 0.5) * 2.0;" : "", + /*$4=*/flip_vertically_ ? "(width_height.y - 1 - gid.y)" : "gid.y", + /*$5=*/ + single_channel + ? "" + : R"(output_data.elements[linear_index + 1] = pixel.y; + output_data.elements[linear_index + 2] = pixel.z;)", + /*$6=*/ + include_alpha ? "output_data.elements[linear_index + 3] = pixel.w;" + : "", + /*$7=*/max_num_channels_); + RET_CHECK_CALL(GlShader::CompileShader(GL_COMPUTE_SHADER, shader_source, + &gpu_data_out_->shader)); + RET_CHECK_CALL(GlProgram::CreateWithShader(gpu_data_out_->shader, + &gpu_data_out_->program)); + return ::mediapipe::OkStatus(); + })); #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS RET_CHECK(include_alpha) << "iOS GPU inference currently accepts only RGBA input."; @@ -546,8 +543,6 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); // with normalization to either: [0,1] or [-1,1]. const std::string shader_source = absl::Substitute( R"( - #include - #include using namespace metal; @@ -612,9 +607,9 @@ REGISTER_CALCULATOR(TfLiteConverterCalculator); // Get desired way to handle input channels. max_num_channels_ = options.max_num_channels(); - // Currently only alpha channel toggling is suppored. - CHECK_GE(max_num_channels_, 3); + CHECK_GE(max_num_channels_, 1); CHECK_LE(max_num_channels_, 4); + CHECK_NE(max_num_channels_, 2); #if defined(__APPLE__) && !TARGET_OS_OSX // iOS if (cc->Inputs().HasTag("IMAGE_GPU")) // Currently on iOS, tflite gpu input tensor must be 4 channels, diff --git a/mediapipe/calculators/tflite/tflite_converter_calculator.proto b/mediapipe/calculators/tflite/tflite_converter_calculator.proto index 3be32b347..2c0d8f4e1 100644 --- a/mediapipe/calculators/tflite/tflite_converter_calculator.proto +++ b/mediapipe/calculators/tflite/tflite_converter_calculator.proto @@ -36,8 +36,7 @@ message TfLiteConverterCalculatorOptions { optional bool flip_vertically = 2 [default = false]; // Controls how many channels of the input image get passed through to the - // tensor. Currently this only controls whether or not to ignore alpha - // channel, so it must be 3 or 4. + // tensor. Valid values are 1,3,4 only. Ignored for iOS GPU. optional int32 max_num_channels = 3 [default = 3]; // The calculator expects Matrix inputs to be in column-major order. Set diff --git a/mediapipe/calculators/tflite/tflite_inference_calculator.cc b/mediapipe/calculators/tflite/tflite_inference_calculator.cc index f9b5646a4..9bc02b48c 100644 --- a/mediapipe/calculators/tflite/tflite_inference_calculator.cc +++ b/mediapipe/calculators/tflite/tflite_inference_calculator.cc @@ -12,10 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include +#include #include #include #include "mediapipe/calculators/tflite/tflite_inference_calculator.pb.h" +#include "mediapipe/calculators/tflite/util.h" #include "mediapipe/framework/calculator_framework.h" #include "mediapipe/framework/port/ret_check.h" #include "mediapipe/util/resource_util.h" @@ -24,14 +27,15 @@ #include "tensorflow/lite/kernels/register.h" #include "tensorflow/lite/model.h" -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) #include "mediapipe/gpu/gl_calculator_helper.h" #include "mediapipe/gpu/gpu_buffer.h" +#include "tensorflow/lite/delegates/gpu/common/shape.h" #include "tensorflow/lite/delegates/gpu/gl/gl_buffer.h" #include "tensorflow/lite/delegates/gpu/gl/gl_program.h" #include "tensorflow/lite/delegates/gpu/gl/gl_shader.h" #include "tensorflow/lite/delegates/gpu/gl_delegate.h" -#endif // __ANDROID__ +#endif // !MEDIAPIPE_DISABLE_GPU #if defined(__APPLE__) && !TARGET_OS_OSX // iOS #import @@ -39,33 +43,42 @@ #import #import "mediapipe/gpu/MPPMetalHelper.h" +#include "mediapipe/gpu/MPPMetalUtil.h" +#include "mediapipe/gpu/gpu_buffer.h" +#include "tensorflow/lite/delegates/gpu/common/shape.h" +#include "tensorflow/lite/delegates/gpu/metal/buffer_convert.h" #include "tensorflow/lite/delegates/gpu/metal_delegate.h" #endif // iOS -#if defined(__ANDROID__) +namespace { + +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) typedef ::tflite::gpu::gl::GlBuffer GpuTensor; #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS typedef id GpuTensor; #endif +// Round up n to next multiple of m. +size_t RoundUp(size_t n, size_t m) { return ((n + m - 1) / m) * m; } // NOLINT +} // namespace + // TfLiteInferenceCalculator File Layout: // * Header // * Core // * Aux namespace mediapipe { -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +using ::tflite::gpu::gl::CopyBuffer; +using ::tflite::gpu::gl::CreateReadWriteShaderStorageBuffer; using ::tflite::gpu::gl::GlBuffer; -using ::tflite::gpu::gl::GlProgram; -using ::tflite::gpu::gl::GlShader; +#endif + +#if !defined(MEDIAPIPE_DISABLE_GPU) struct GPUData { int elements = 1; - GlBuffer buffer; -}; -#elif defined(__APPLE__) && !TARGET_OS_OSX // iOS -struct GPUData { - int elements = 1; - id buffer; + GpuTensor buffer; + ::tflite::gpu::BHWC shape; }; #endif @@ -134,7 +147,7 @@ class TfLiteInferenceCalculator : public CalculatorBase { std::unique_ptr model_; TfLiteDelegate* delegate_ = nullptr; -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) mediapipe::GlCalculatorHelper gpu_helper_; std::unique_ptr gpu_data_in_; std::vector> gpu_data_out_; @@ -142,6 +155,7 @@ class TfLiteInferenceCalculator : public CalculatorBase { MPPMetalHelper* gpu_helper_ = nullptr; std::unique_ptr gpu_data_in_; std::vector> gpu_data_out_; + TFLBufferConvert* converter_from_BPHWC4_ = nil; #endif std::string model_path_ = ""; @@ -161,19 +175,25 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); RET_CHECK(cc->Outputs().HasTag("TENSORS") ^ cc->Outputs().HasTag("TENSORS_GPU")); + bool use_gpu = false; + if (cc->Inputs().HasTag("TENSORS")) cc->Inputs().Tag("TENSORS").Set>(); -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) - if (cc->Inputs().HasTag("TENSORS_GPU")) +#if !defined(MEDIAPIPE_DISABLE_GPU) + if (cc->Inputs().HasTag("TENSORS_GPU")) { cc->Inputs().Tag("TENSORS_GPU").Set>(); -#endif + use_gpu |= true; + } +#endif // !MEDIAPIPE_DISABLE_GPU if (cc->Outputs().HasTag("TENSORS")) cc->Outputs().Tag("TENSORS").Set>(); -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) - if (cc->Outputs().HasTag("TENSORS_GPU")) +#if !defined(MEDIAPIPE_DISABLE_GPU) + if (cc->Outputs().HasTag("TENSORS_GPU")) { cc->Outputs().Tag("TENSORS_GPU").Set>(); -#endif + use_gpu |= true; + } +#endif // !MEDIAPIPE_DISABLE_GPU if (cc->InputSidePackets().HasTag("CUSTOM_OP_RESOLVER")) { cc->InputSidePackets() @@ -181,11 +201,17 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); .Set(); } -#if defined(__ANDROID__) - MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); + const auto& options = + cc->Options<::mediapipe::TfLiteInferenceCalculatorOptions>(); + use_gpu |= options.use_gpu(); + + if (use_gpu) { +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) + MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS - MP_RETURN_IF_ERROR([MPPMetalHelper updateContract:cc]); + MP_RETURN_IF_ERROR([MPPMetalHelper updateContract:cc]); #endif + } // Assign this calculator's default InputStreamHandler. cc->SetInputStreamHandler("FixedSizeInputStreamHandler"); @@ -199,35 +225,41 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); MP_RETURN_IF_ERROR(LoadOptions(cc)); if (cc->Inputs().HasTag("TENSORS_GPU")) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) gpu_input_ = true; gpu_inference_ = true; // Inference must be on GPU also. #else - RET_CHECK_FAIL() << "GPU processing is for Android and iOS only."; -#endif + RET_CHECK(!cc->Inputs().HasTag("TENSORS_GPU")) + << "GPU processing not enabled."; +#endif // !MEDIAPIPE_DISABLE_GPU } if (cc->Outputs().HasTag("TENSORS_GPU")) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) gpu_output_ = true; RET_CHECK(cc->Inputs().HasTag("TENSORS_GPU")) << "GPU output must also have GPU Input."; #else - RET_CHECK_FAIL() << "GPU processing is for Android and iOS only."; -#endif + RET_CHECK(!cc->Inputs().HasTag("TENSORS_GPU")) + << "GPU processing not enabled."; +#endif // !MEDIAPIPE_DISABLE_GPU } MP_RETURN_IF_ERROR(LoadModel(cc)); if (gpu_inference_) { -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !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__) + MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext( + [this, &cc]() -> ::mediapipe::Status { return LoadDelegate(cc); })); +#else MP_RETURN_IF_ERROR(LoadDelegate(cc)); +#endif } return ::mediapipe::OkStatus(); @@ -237,35 +269,27 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); // 1. Receive pre-processed tensor inputs. if (gpu_input_) { // Read GPU input into SSBO. -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) const auto& input_tensors = cc->Inputs().Tag("TENSORS_GPU").Get>(); RET_CHECK_EQ(input_tensors.size(), 1); MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext( [this, &input_tensors]() -> ::mediapipe::Status { // Explicit copy input. - tflite::gpu::gl::CopyBuffer(input_tensors[0], gpu_data_in_->buffer); + RET_CHECK_CALL(CopyBuffer(input_tensors[0], gpu_data_in_->buffer)); return ::mediapipe::OkStatus(); })); #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS const auto& input_tensors = cc->Inputs().Tag("TENSORS_GPU").Get>(); RET_CHECK_EQ(input_tensors.size(), 1); - id command_buffer = [gpu_helper_ commandBuffer]; - command_buffer.label = @"TfLiteInferenceCalculatorInput"; - id blit_command = - [command_buffer blitCommandEncoder]; // Explicit copy input. - [blit_command copyFromBuffer:input_tensors[0] - sourceOffset:0 - toBuffer:gpu_data_in_->buffer - destinationOffset:0 - size:gpu_data_in_->elements * sizeof(float)]; - [blit_command endEncoding]; - [command_buffer commit]; - [command_buffer waitUntilCompleted]; + [MPPMetalUtil blitMetalBufferTo:gpu_data_in_->buffer + from:input_tensors[0] + blocking:true + commandBuffer:[gpu_helper_ commandBuffer]]; #else - RET_CHECK_FAIL() << "GPU processing is for Android and iOS only."; + RET_CHECK_FAIL() << "GPU processing not enabled."; #endif } else { // Read CPU input into tensors. @@ -278,18 +302,20 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); if (use_quantized_tensors_) { const uint8* input_tensor_buffer = input_tensor->data.uint8; uint8* local_tensor_buffer = interpreter_->typed_input_tensor(i); - memcpy(local_tensor_buffer, input_tensor_buffer, input_tensor->bytes); + std::memcpy(local_tensor_buffer, input_tensor_buffer, + input_tensor->bytes); } else { const float* input_tensor_buffer = input_tensor->data.f; float* local_tensor_buffer = interpreter_->typed_input_tensor(i); - memcpy(local_tensor_buffer, input_tensor_buffer, input_tensor->bytes); + std::memcpy(local_tensor_buffer, input_tensor_buffer, + input_tensor->bytes); } } } // 2. Run inference. if (gpu_inference_) { -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) MP_RETURN_IF_ERROR( gpu_helper_.RunInGlContext([this]() -> ::mediapipe::Status { RET_CHECK_EQ(interpreter_->Invoke(), kTfLiteOk); @@ -304,52 +330,51 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); // 3. Output processed tensors. if (gpu_output_) { -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) // Output result tensors (GPU). auto output_tensors = absl::make_unique>(); - output_tensors->resize(gpu_data_out_.size()); - for (int i = 0; i < gpu_data_out_.size(); ++i) { - GlBuffer& tensor = output_tensors->at(i); - using ::tflite::gpu::gl::CreateReadWriteShaderStorageBuffer; - auto status = CreateReadWriteShaderStorageBuffer( - gpu_data_out_[i]->elements, &tensor); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } - tflite::gpu::gl::CopyBuffer(gpu_data_out_[i]->buffer, tensor); - } + MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext( + [this, &output_tensors]() -> ::mediapipe::Status { + output_tensors->resize(gpu_data_out_.size()); + for (int i = 0; i < gpu_data_out_.size(); ++i) { + GpuTensor& tensor = output_tensors->at(i); + RET_CHECK_CALL(CreateReadWriteShaderStorageBuffer( + gpu_data_out_[i]->elements, &tensor)); + RET_CHECK_CALL(CopyBuffer(gpu_data_out_[i]->buffer, tensor)); + } + return ::mediapipe::OkStatus(); + })); cc->Outputs() .Tag("TENSORS_GPU") .Add(output_tensors.release(), cc->InputTimestamp()); #elif defined(__APPLE__) && !TARGET_OS_OSX // iOS // Output result tensors (GPU). auto output_tensors = absl::make_unique>(); + output_tensors->resize(gpu_data_out_.size()); id device = gpu_helper_.mtlDevice; id command_buffer = [gpu_helper_ commandBuffer]; - command_buffer.label = @"TfLiteInferenceCalculatorOutput"; + command_buffer.label = @"TfLiteInferenceBPHWC4Convert"; + id convert_command = + [command_buffer computeCommandEncoder]; for (int i = 0; i < gpu_data_out_.size(); ++i) { - id tensor = + output_tensors->at(i) = [device newBufferWithLength:gpu_data_out_[i]->elements * sizeof(float) options:MTLResourceStorageModeShared]; - id blit_command = - [command_buffer blitCommandEncoder]; - // Explicit copy input. - [blit_command copyFromBuffer:gpu_data_out_[i]->buffer - sourceOffset:0 - toBuffer:tensor - destinationOffset:0 - size:gpu_data_out_[i]->elements * sizeof(float)]; - [blit_command endEncoding]; - [command_buffer commit]; - [command_buffer waitUntilCompleted]; - output_tensors->push_back(tensor); + // Reshape tensor. + [converter_from_BPHWC4_ convertWithEncoder:convert_command + shape:gpu_data_out_[i]->shape + sourceBuffer:gpu_data_out_[i]->buffer + convertedBuffer:output_tensors->at(i)]; } + [convert_command endEncoding]; + [command_buffer commit]; + [command_buffer waitUntilCompleted]; cc->Outputs() .Tag("TENSORS_GPU") .Add(output_tensors.release(), cc->InputTimestamp()); #else - RET_CHECK_FAIL() << "GPU processing is for Android and iOS only."; -#endif + RET_CHECK_FAIL() << "GPU processing not enabled."; +#endif // !MEDIAPIPE_DISABLE_GPU } else { // Output result tensors (CPU). const auto& tensor_indexes = interpreter_->outputs(); @@ -367,7 +392,7 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); ::mediapipe::Status TfLiteInferenceCalculator::Close(CalculatorContext* cc) { if (delegate_) { -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext([this]() -> Status { TfLiteGpuDelegateDelete(delegate_); gpu_data_in_.reset(); @@ -446,7 +471,7 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); ::mediapipe::Status TfLiteInferenceCalculator::LoadDelegate( CalculatorContext* cc) { -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) // Configure and create the delegate. TfLiteGpuDelegateOptions options = TfLiteGpuDelegateOptionsDefault(); options.compile_options.precision_loss_allowed = 1; @@ -466,15 +491,12 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); for (int d = 0; d < tensor->dims->size; ++d) { gpu_data_in_->elements *= tensor->dims->data[d]; } - // Input to model can be either RGB/RGBA only. - RET_CHECK_GE(tensor->dims->data[3], 3); - RET_CHECK_LE(tensor->dims->data[3], 4); + CHECK_GE(tensor->dims->data[3], 1); + CHECK_LE(tensor->dims->data[3], 4); + CHECK_NE(tensor->dims->data[3], 2); // Create and bind input buffer. - auto status = ::tflite::gpu::gl::CreateReadWriteShaderStorageBuffer( - gpu_data_in_->elements, &gpu_data_in_->buffer); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } + RET_CHECK_CALL(::tflite::gpu::gl::CreateReadWriteShaderStorageBuffer( + gpu_data_in_->elements, &gpu_data_in_->buffer)); RET_CHECK_EQ(TfLiteGpuDelegateBindBufferToTensor( delegate_, gpu_data_in_->buffer.id(), interpreter_->inputs()[0]), // First tensor only @@ -496,12 +518,8 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); // Create and bind output buffers. interpreter_->SetAllowBufferHandleOutput(true); for (int i = 0; i < gpu_data_out_.size(); ++i) { - using ::tflite::gpu::gl::CreateReadWriteShaderStorageBuffer; - auto status = CreateReadWriteShaderStorageBuffer( - gpu_data_out_[i]->elements, &gpu_data_out_[i]->buffer); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } + RET_CHECK_CALL(CreateReadWriteShaderStorageBuffer( + gpu_data_out_[i]->elements, &gpu_data_out_[i]->buffer)); RET_CHECK_EQ( TfLiteGpuDelegateBindBufferToTensor( delegate_, gpu_data_out_[i]->buffer.id(), output_indices[i]), @@ -511,14 +529,15 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); // Must call this last. RET_CHECK_EQ(interpreter_->ModifyGraphWithDelegate(delegate_), kTfLiteOk); -#endif // __ANDROID__ +#endif // OpenGL #if defined(__APPLE__) && !TARGET_OS_OSX // iOS // Configure and create the delegate. GpuDelegateOptions options; options.allow_precision_loss = false; // Must match converter, F=float/T=half - options.wait_type = GpuDelegateOptions::WaitType::kActive; + options.wait_type = GpuDelegateOptions::WaitType::kPassive; if (!delegate_) delegate_ = TFLGpuDelegateCreate(&options); + id device = gpu_helper_.mtlDevice; if (gpu_input_) { // Get input image sizes. @@ -539,11 +558,9 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); LOG(WARNING) << "Please ensure input GPU tensor is 4 channels."; } // Create and bind input buffer. - id device = gpu_helper_.mtlDevice; gpu_data_in_->buffer = [device newBufferWithLength:gpu_data_in_->elements * sizeof(float) options:MTLResourceStorageModeShared]; - // Must call this before TFLGpuDelegateBindMetalBufferToTensor. RET_CHECK_EQ(interpreter_->ModifyGraphWithDelegate(delegate_), kTfLiteOk); RET_CHECK_EQ(TFLGpuDelegateBindMetalBufferToTensor( delegate_, @@ -561,12 +578,33 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); gpu_data_out_[i]->elements = 1; // TODO handle *2 properly on some dialated models for (int d = 0; d < tensor->dims->size; ++d) { - gpu_data_out_[i]->elements *= tensor->dims->data[d]; + // Pad each dim for BHWC4 conversion inside delegate. + gpu_data_out_[i]->elements *= RoundUp(tensor->dims->data[d], 4); + } + // Save dimensions for reshaping back later. + gpu_data_out_[i]->shape.b = tensor->dims->data[0]; + switch (tensor->dims->size) { + case 2: + gpu_data_out_[i]->shape.h = 1; + gpu_data_out_[i]->shape.w = 1; + gpu_data_out_[i]->shape.c = tensor->dims->data[1]; + break; + case 3: + gpu_data_out_[i]->shape.h = 1; + gpu_data_out_[i]->shape.w = tensor->dims->data[1]; + gpu_data_out_[i]->shape.c = tensor->dims->data[2]; + break; + case 4: + gpu_data_out_[i]->shape.h = tensor->dims->data[1]; + gpu_data_out_[i]->shape.w = tensor->dims->data[2]; + gpu_data_out_[i]->shape.c = tensor->dims->data[3]; + break; + default: + return mediapipe::InternalError("Unsupported tensor shape."); } } // Create and bind output buffers. interpreter_->SetAllowBufferHandleOutput(true); - id device = gpu_helper_.mtlDevice; for (int i = 0; i < gpu_data_out_.size(); ++i) { gpu_data_out_[i]->buffer = [device newBufferWithLength:gpu_data_out_[i]->elements * sizeof(float) @@ -575,6 +613,14 @@ REGISTER_CALCULATOR(TfLiteInferenceCalculator); delegate_, output_indices[i], gpu_data_out_[i]->buffer), true); } + // Create converter for GPU output. + converter_from_BPHWC4_ = [[TFLBufferConvert alloc] initWithDevice:device + isFloat16:false + convertToPBHWC4:false]; + if (converter_from_BPHWC4_ == nil) { + return mediapipe::InternalError( + "Error initializating output buffer converter"); + } } #endif // iOS diff --git a/mediapipe/calculators/tflite/tflite_tensors_to_detections_calculator.cc b/mediapipe/calculators/tflite/tflite_tensors_to_detections_calculator.cc index 057a1831b..8e790b00a 100644 --- a/mediapipe/calculators/tflite/tflite_tensors_to_detections_calculator.cc +++ b/mediapipe/calculators/tflite/tflite_tensors_to_detections_calculator.cc @@ -18,6 +18,7 @@ #include "absl/strings/str_format.h" #include "absl/types/span.h" #include "mediapipe/calculators/tflite/tflite_tensors_to_detections_calculator.pb.h" +#include "mediapipe/calculators/tflite/util.h" #include "mediapipe/framework/calculator_framework.h" #include "mediapipe/framework/deps/file_path.h" #include "mediapipe/framework/formats/detection.pb.h" @@ -26,28 +27,61 @@ #include "mediapipe/framework/port/ret_check.h" #include "tensorflow/lite/interpreter.h" -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !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" #include "tensorflow/lite/delegates/gpu/gl/gl_shader.h" #include "tensorflow/lite/delegates/gpu/gl_delegate.h" -#endif // ANDROID +#endif // !MEDIAPIPE_DISABLE_GPU -#if defined(__ANDROID__) -using ::tflite::gpu::gl::CreateReadWriteShaderStorageBuffer; -using ::tflite::gpu::gl::GlBuffer; -using ::tflite::gpu::gl::GlProgram; -using ::tflite::gpu::gl::GlShader; -#endif // ANDROID +#if defined(__APPLE__) && !TARGET_OS_OSX // iOS +#import +#import +#import -namespace mediapipe { +#import "mediapipe/gpu/MPPMetalHelper.h" +#include "mediapipe/gpu/MPPMetalUtil.h" +#include "mediapipe/gpu/gpu_buffer.h" +#include "tensorflow/lite/delegates/gpu/metal_delegate.h" +#endif // iOS namespace { constexpr int kNumInputTensorsWithAnchors = 3; constexpr int kNumCoordsPerBox = 4; +} // namespace + +namespace mediapipe { + +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +using ::tflite::gpu::gl::CreateReadWriteShaderStorageBuffer; +using ::tflite::gpu::gl::GlShader; +#endif + +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +typedef ::tflite::gpu::gl::GlBuffer GpuTensor; +typedef ::tflite::gpu::gl::GlProgram GpuProgram; +#elif defined(__APPLE__) && !TARGET_OS_OSX // iOS +typedef id GpuTensor; +typedef id GpuProgram; +#endif + +namespace { + +#if !defined(MEDIAPIPE_DISABLE_GPU) +struct GPUData { + GpuProgram decode_program; + GpuProgram score_program; + GpuTensor decoded_boxes_buffer; + GpuTensor raw_boxes_buffer; + GpuTensor raw_anchors_buffer; + GpuTensor scored_boxes_buffer; + GpuTensor raw_scores_buffer; +}; +#endif + void ConvertRawValuesToAnchors(const float* raw_anchors, int num_boxes, std::vector* anchors) { anchors->clear(); @@ -88,7 +122,7 @@ void ConvertAnchorsToRawValues(const std::vector& anchors, // optional to pass in a third tensor for anchors (e.g. for SSD // models) depend on the outputs of the detection model. The size // of anchor tensor must be (num_boxes * 4). -// TENSORS_GPU - vector of GlBuffer. +// TENSORS_GPU - vector of GlBuffer of MTLBuffer. // Output: // DETECTIONS - Result MediaPipe detections. // @@ -126,7 +160,7 @@ class TfLiteTensorsToDetectionsCalculator : public CalculatorBase { std::vector* output_detections); ::mediapipe::Status LoadOptions(CalculatorContext* cc); - ::mediapipe::Status GlSetup(CalculatorContext* cc); + ::mediapipe::Status GpuInit(CalculatorContext* cc); ::mediapipe::Status DecodeBoxes(const float* raw_boxes, const std::vector& anchors, std::vector* boxes); @@ -146,15 +180,12 @@ class TfLiteTensorsToDetectionsCalculator : public CalculatorBase { std::vector anchors_; bool side_packet_anchors_{}; -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) mediapipe::GlCalculatorHelper gpu_helper_; - std::unique_ptr decode_program_; - std::unique_ptr score_program_; - std::unique_ptr decoded_boxes_buffer_; - std::unique_ptr raw_boxes_buffer_; - std::unique_ptr raw_anchors_buffer_; - std::unique_ptr scored_boxes_buffer_; - std::unique_ptr raw_scores_buffer_; + std::unique_ptr gpu_data_; +#elif defined(__APPLE__) && !TARGET_OS_OSX // iOS + MPPMetalHelper* gpu_helper_ = nullptr; + std::unique_ptr gpu_data_; #endif bool gpu_input_ = false; @@ -167,15 +198,18 @@ REGISTER_CALCULATOR(TfLiteTensorsToDetectionsCalculator); RET_CHECK(!cc->Inputs().GetTags().empty()); RET_CHECK(!cc->Outputs().GetTags().empty()); + bool use_gpu = false; + if (cc->Inputs().HasTag("TENSORS")) { cc->Inputs().Tag("TENSORS").Set>(); } -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) if (cc->Inputs().HasTag("TENSORS_GPU")) { - cc->Inputs().Tag("TENSORS_GPU").Set>(); + cc->Inputs().Tag("TENSORS_GPU").Set>(); + use_gpu |= true; } -#endif +#endif // !MEDIAPIPE_DISABLE_GPU if (cc->Outputs().HasTag("DETECTIONS")) { cc->Outputs().Tag("DETECTIONS").Set>(); @@ -187,9 +221,13 @@ REGISTER_CALCULATOR(TfLiteTensorsToDetectionsCalculator); } } -#if defined(__ANDROID__) - MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); + if (use_gpu) { +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) + MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); +#elif defined(__APPLE__) && !TARGET_OS_OSX // iOS + MP_RETURN_IF_ERROR([MPPMetalHelper updateContract:cc]); #endif + } return ::mediapipe::OkStatus(); } @@ -200,8 +238,11 @@ REGISTER_CALCULATOR(TfLiteTensorsToDetectionsCalculator); if (cc->Inputs().HasTag("TENSORS_GPU")) { gpu_input_ = true; -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !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 } @@ -209,7 +250,7 @@ REGISTER_CALCULATOR(TfLiteTensorsToDetectionsCalculator); side_packet_anchors_ = cc->InputSidePackets().HasTag("ANCHORS"); if (gpu_input_) { - MP_RETURN_IF_ERROR(GlSetup(cc)); + MP_RETURN_IF_ERROR(GpuInit(cc)); } return ::mediapipe::OkStatus(); @@ -228,7 +269,7 @@ REGISTER_CALCULATOR(TfLiteTensorsToDetectionsCalculator); MP_RETURN_IF_ERROR(ProcessGPU(cc, output_detections.get())); } else { MP_RETURN_IF_ERROR(ProcessCPU(cc, output_detections.get())); - } // if gpu_input_ + } // Output if (cc->Outputs().HasTag("DETECTIONS")) { @@ -245,7 +286,8 @@ REGISTER_CALCULATOR(TfLiteTensorsToDetectionsCalculator); const auto& input_tensors = cc->Inputs().Tag("TENSORS").Get>(); - if (input_tensors.size() == 2) { + if (input_tensors.size() == 2 || + input_tensors.size() == kNumInputTensorsWithAnchors) { // Postprocessing on CPU for model without postprocessing op. E.g. output // raw score tensor and box tensor. Anchor decoding will be handled below. const TfLiteTensor* raw_box_tensor = &input_tensors[0]; @@ -358,13 +400,84 @@ REGISTER_CALCULATOR(TfLiteTensorsToDetectionsCalculator); } ::mediapipe::Status TfLiteTensorsToDetectionsCalculator::ProcessGPU( CalculatorContext* cc, std::vector* output_detections) { -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) const auto& input_tensors = - cc->Inputs().Tag("TENSORS_GPU").Get>(); + cc->Inputs().Tag("TENSORS_GPU").Get>(); + RET_CHECK_GE(input_tensors.size(), 2); + + MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext([this, &input_tensors, &cc, + &output_detections]() + -> ::mediapipe::Status { + // Copy inputs. + RET_CHECK_CALL(CopyBuffer(input_tensors[0], gpu_data_->raw_boxes_buffer)); + RET_CHECK_CALL(CopyBuffer(input_tensors[1], gpu_data_->raw_scores_buffer)); + if (!anchors_init_) { + if (side_packet_anchors_) { + CHECK(!cc->InputSidePackets().Tag("ANCHORS").IsEmpty()); + const auto& anchors = + cc->InputSidePackets().Tag("ANCHORS").Get>(); + std::vector raw_anchors(num_boxes_ * kNumCoordsPerBox); + ConvertAnchorsToRawValues(anchors, num_boxes_, raw_anchors.data()); + RET_CHECK_CALL(gpu_data_->raw_anchors_buffer.Write( + absl::MakeSpan(raw_anchors))); + } else { + CHECK_EQ(input_tensors.size(), kNumInputTensorsWithAnchors); + RET_CHECK_CALL( + CopyBuffer(input_tensors[2], gpu_data_->raw_anchors_buffer)); + } + anchors_init_ = true; + } + + // Run shaders. + // Decode boxes. + RET_CHECK_CALL(gpu_data_->decoded_boxes_buffer.BindToIndex(0)); + RET_CHECK_CALL(gpu_data_->raw_boxes_buffer.BindToIndex(1)); + RET_CHECK_CALL(gpu_data_->raw_anchors_buffer.BindToIndex(2)); + const tflite::gpu::uint3 decode_workgroups = {num_boxes_, 1, 1}; + RET_CHECK_CALL(gpu_data_->decode_program.Dispatch(decode_workgroups)); + + // Score boxes. + RET_CHECK_CALL(gpu_data_->scored_boxes_buffer.BindToIndex(0)); + RET_CHECK_CALL(gpu_data_->raw_scores_buffer.BindToIndex(1)); + const tflite::gpu::uint3 score_workgroups = {num_boxes_, 1, 1}; + RET_CHECK_CALL(gpu_data_->score_program.Dispatch(score_workgroups)); + + // Copy decoded boxes from GPU to CPU. + std::vector boxes(num_boxes_ * num_coords_); + RET_CHECK_CALL(gpu_data_->decoded_boxes_buffer.Read(absl::MakeSpan(boxes))); + std::vector score_class_id_pairs(num_boxes_ * 2); + RET_CHECK_CALL(gpu_data_->scored_boxes_buffer.Read( + absl::MakeSpan(score_class_id_pairs))); + + // TODO: b/138851969. Is it possible to output a float vector + // for score and an int vector for class so that we can avoid copying twice? + std::vector detection_scores(num_boxes_); + std::vector detection_classes(num_boxes_); + for (int i = 0; i < num_boxes_; ++i) { + detection_scores[i] = score_class_id_pairs[i * 2]; + detection_classes[i] = static_cast(score_class_id_pairs[i * 2 + 1]); + } + MP_RETURN_IF_ERROR( + ConvertToDetections(boxes.data(), detection_scores.data(), + detection_classes.data(), output_detections)); + + return ::mediapipe::OkStatus(); + })); +#elif defined(__APPLE__) && !TARGET_OS_OSX // iOS + + const auto& input_tensors = + cc->Inputs().Tag("TENSORS_GPU").Get>(); + RET_CHECK_GE(input_tensors.size(), 2); // Copy inputs. - tflite::gpu::gl::CopyBuffer(input_tensors[0], *raw_boxes_buffer_.get()); - tflite::gpu::gl::CopyBuffer(input_tensors[1], *raw_scores_buffer_.get()); + [MPPMetalUtil blitMetalBufferTo:gpu_data_->raw_boxes_buffer + from:input_tensors[0] + blocking:true + commandBuffer:[gpu_helper_ commandBuffer]]; + [MPPMetalUtil blitMetalBufferTo:gpu_data_->raw_scores_buffer + from:input_tensors[1] + blocking:true + commandBuffer:[gpu_helper_ commandBuffer]]; if (!anchors_init_) { if (side_packet_anchors_) { CHECK(!cc->InputSidePackets().Tag("ANCHORS").IsEmpty()); @@ -372,47 +485,65 @@ REGISTER_CALCULATOR(TfLiteTensorsToDetectionsCalculator); cc->InputSidePackets().Tag("ANCHORS").Get>(); std::vector raw_anchors(num_boxes_ * kNumCoordsPerBox); ConvertAnchorsToRawValues(anchors, num_boxes_, raw_anchors.data()); - raw_anchors_buffer_->Write(absl::MakeSpan(raw_anchors)); + memcpy([gpu_data_->raw_anchors_buffer contents], raw_anchors.data(), + raw_anchors.size() * sizeof(float)); } else { - CHECK_EQ(input_tensors.size(), 3); - tflite::gpu::gl::CopyBuffer(input_tensors[2], *raw_anchors_buffer_.get()); + RET_CHECK_EQ(input_tensors.size(), kNumInputTensorsWithAnchors); + [MPPMetalUtil blitMetalBufferTo:gpu_data_->raw_anchors_buffer + from:input_tensors[2] + blocking:true + commandBuffer:[gpu_helper_ commandBuffer]]; } anchors_init_ = true; } // Run shaders. - MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext( - [this, &input_tensors]() -> ::mediapipe::Status { - // Decode boxes. - decoded_boxes_buffer_->BindToIndex(0); - raw_boxes_buffer_->BindToIndex(1); - raw_anchors_buffer_->BindToIndex(2); - const tflite::gpu::uint3 decode_workgroups = {num_boxes_, 1, 1}; - decode_program_->Dispatch(decode_workgroups); - - // Score boxes. - scored_boxes_buffer_->BindToIndex(0); - raw_scores_buffer_->BindToIndex(1); - const tflite::gpu::uint3 score_workgroups = {num_boxes_, 1, 1}; - score_program_->Dispatch(score_workgroups); - - return ::mediapipe::OkStatus(); - })); + { + id command_buffer = [gpu_helper_ commandBuffer]; + command_buffer.label = @"TfLiteDecodeBoxes"; + id decode_command = + [command_buffer computeCommandEncoder]; + [decode_command setComputePipelineState:gpu_data_->decode_program]; + [decode_command setBuffer:gpu_data_->decoded_boxes_buffer + offset:0 + atIndex:0]; + [decode_command setBuffer:gpu_data_->raw_boxes_buffer offset:0 atIndex:1]; + [decode_command setBuffer:gpu_data_->raw_anchors_buffer offset:0 atIndex:2]; + MTLSize decode_threads_per_group = MTLSizeMake(1, 1, 1); + MTLSize decode_threadgroups = MTLSizeMake(num_boxes_, 1, 1); + [decode_command dispatchThreadgroups:decode_threadgroups + threadsPerThreadgroup:decode_threads_per_group]; + [decode_command endEncoding]; + [command_buffer commit]; + [command_buffer waitUntilCompleted]; + } + { + id command_buffer = [gpu_helper_ commandBuffer]; + command_buffer.label = @"TfLiteScoreBoxes"; + id score_command = + [command_buffer computeCommandEncoder]; + [score_command setComputePipelineState:gpu_data_->score_program]; + [score_command setBuffer:gpu_data_->scored_boxes_buffer offset:0 atIndex:0]; + [score_command setBuffer:gpu_data_->raw_scores_buffer offset:0 atIndex:1]; + MTLSize score_threads_per_group = MTLSizeMake(1, num_classes_, 1); + MTLSize score_threadgroups = MTLSizeMake(num_boxes_, 1, 1); + [score_command dispatchThreadgroups:score_threadgroups + threadsPerThreadgroup:score_threads_per_group]; + [score_command endEncoding]; + [command_buffer commit]; + [command_buffer waitUntilCompleted]; + } // Copy decoded boxes from GPU to CPU. std::vector boxes(num_boxes_ * num_coords_); - auto status = decoded_boxes_buffer_->Read(absl::MakeSpan(boxes)); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } + memcpy(boxes.data(), [gpu_data_->decoded_boxes_buffer contents], + num_boxes_ * num_coords_ * sizeof(float)); std::vector score_class_id_pairs(num_boxes_ * 2); - status = scored_boxes_buffer_->Read(absl::MakeSpan(score_class_id_pairs)); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } + memcpy(score_class_id_pairs.data(), [gpu_data_->scored_boxes_buffer contents], + num_boxes_ * 2 * sizeof(float)); - // TODO: b/138851969. Is it possible to output a float vector - // for score and an int vector for class so that we can avoid copying twice? + // Output detections. + // TODO Adjust shader to avoid copying shader output twice. std::vector detection_scores(num_boxes_); std::vector detection_classes(num_boxes_); for (int i = 0; i < num_boxes_; ++i) { @@ -422,25 +553,20 @@ REGISTER_CALCULATOR(TfLiteTensorsToDetectionsCalculator); MP_RETURN_IF_ERROR(ConvertToDetections(boxes.data(), detection_scores.data(), detection_classes.data(), output_detections)); + #else LOG(ERROR) << "GPU input on non-Android not supported yet."; -#endif // defined(__ANDROID__) +#endif return ::mediapipe::OkStatus(); } ::mediapipe::Status TfLiteTensorsToDetectionsCalculator::Close( CalculatorContext* cc) { -#if defined(__ANDROID__) - gpu_helper_.RunInGlContext([this] { - decode_program_.reset(); - score_program_.reset(); - decoded_boxes_buffer_.reset(); - raw_boxes_buffer_.reset(); - raw_anchors_buffer_.reset(); - scored_boxes_buffer_.reset(); - raw_scores_buffer_.reset(); - }); -#endif // __ANDROID__ +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) + gpu_helper_.RunInGlContext([this] { gpu_data_.reset(); }); +#elif defined(__APPLE__) && !TARGET_OS_OSX // iOS + gpu_data_.reset(); +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } @@ -530,6 +656,7 @@ REGISTER_CALCULATOR(TfLiteTensorsToDetectionsCalculator); } } } + return ::mediapipe::OkStatus(); } @@ -586,12 +713,16 @@ Detection TfLiteTensorsToDetectionsCalculator::ConvertToDetection( return detection; } -::mediapipe::Status TfLiteTensorsToDetectionsCalculator::GlSetup( +::mediapipe::Status TfLiteTensorsToDetectionsCalculator::GpuInit( CalculatorContext* cc) { -#if defined(__ANDROID__) - // A shader to decode detection boxes. - const std::string decode_src = absl::Substitute( - R"( #version 310 es +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) + MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext([this]() + -> ::mediapipe::Status { + gpu_data_ = absl::make_unique(); + + // A shader to decode detection boxes. + const std::string decode_src = absl::Substitute( + R"( #version 310 es layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; @@ -665,7 +796,7 @@ void main() { if (num_keypoints > int(0)){ for (int k = 0; k < num_keypoints; ++k) { int kp_offset = - int(g_idx * num_coords) + keypt_coord_offset + k * num_values_per_keypt; + int(g_idx * num_coords) + keypt_coord_offset + k * num_values_per_keypt; float kp_y, kp_x; if (reverse_output_order == int(0)) { kp_y = raw_boxes.data[kp_offset + int(0)]; @@ -679,55 +810,37 @@ void main() { } } })", - options_.num_coords(), // box xywh - options_.reverse_output_order() ? 1 : 0, - options_.apply_exponential_on_box_size() ? 1 : 0, - options_.box_coord_offset(), options_.num_keypoints(), - options_.keypoint_coord_offset(), options_.num_values_per_keypoint()); + options_.num_coords(), // box xywh + options_.reverse_output_order() ? 1 : 0, + options_.apply_exponential_on_box_size() ? 1 : 0, + options_.box_coord_offset(), options_.num_keypoints(), + options_.keypoint_coord_offset(), options_.num_values_per_keypoint()); - // Shader program - GlShader decode_shader; - auto status = - GlShader::CompileShader(GL_COMPUTE_SHADER, decode_src, &decode_shader); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } - decode_program_ = absl::make_unique(); - status = GlProgram::CreateWithShader(decode_shader, decode_program_.get()); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } - // Outputs - size_t decoded_boxes_length = num_boxes_ * num_coords_; - decoded_boxes_buffer_ = absl::make_unique(); - status = CreateReadWriteShaderStorageBuffer( - decoded_boxes_length, decoded_boxes_buffer_.get()); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } - // Inputs - size_t raw_boxes_length = num_boxes_ * num_coords_; - raw_boxes_buffer_ = absl::make_unique(); - status = CreateReadWriteShaderStorageBuffer(raw_boxes_length, - raw_boxes_buffer_.get()); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } - size_t raw_anchors_length = num_boxes_ * kNumCoordsPerBox; - raw_anchors_buffer_ = absl::make_unique(); - status = CreateReadWriteShaderStorageBuffer(raw_anchors_length, - raw_anchors_buffer_.get()); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } - // Parameters - glUseProgram(decode_program_->id()); - glUniform4f(0, options_.x_scale(), options_.y_scale(), options_.w_scale(), - options_.h_scale()); + // Shader program + GlShader decode_shader; + RET_CHECK_CALL( + GlShader::CompileShader(GL_COMPUTE_SHADER, decode_src, &decode_shader)); + RET_CHECK_CALL(GpuProgram::CreateWithShader(decode_shader, + &gpu_data_->decode_program)); + // Outputs + size_t decoded_boxes_length = num_boxes_ * num_coords_; + RET_CHECK_CALL(CreateReadWriteShaderStorageBuffer( + decoded_boxes_length, &gpu_data_->decoded_boxes_buffer)); + // Inputs + size_t raw_boxes_length = num_boxes_ * num_coords_; + RET_CHECK_CALL(CreateReadWriteShaderStorageBuffer( + raw_boxes_length, &gpu_data_->raw_boxes_buffer)); + size_t raw_anchors_length = num_boxes_ * kNumCoordsPerBox; + RET_CHECK_CALL(CreateReadWriteShaderStorageBuffer( + raw_anchors_length, &gpu_data_->raw_anchors_buffer)); + // Parameters + glUseProgram(gpu_data_->decode_program.id()); + glUniform4f(0, options_.x_scale(), options_.y_scale(), options_.w_scale(), + options_.h_scale()); - // A shader to score detection boxes. - const std::string score_src = absl::Substitute( - R"( #version 310 es + // A shader to score detection boxes. + const std::string score_src = absl::Substitute( + R"( #version 310 es layout(local_size_x = 1, local_size_y = $0, local_size_z = 1) in; @@ -781,6 +894,228 @@ void main() { scored_boxes.data[g_idx * uint(2) + uint(0)] = max_score; scored_boxes.data[g_idx * uint(2) + uint(1)] = max_class; } +})", + num_classes_, options_.sigmoid_score() ? 1 : 0, + options_.has_score_clipping_thresh() ? 1 : 0, + options_.has_score_clipping_thresh() ? options_.score_clipping_thresh() + : 0, + !ignore_classes_.empty() ? 1 : 0); + + // # filter classes supported is hardware dependent. + int max_wg_size; // typically <= 1024 + glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, 1, + &max_wg_size); // y-dim + CHECK_LT(num_classes_, max_wg_size) + << "# classes must be < " << max_wg_size; + // TODO support better filtering. + CHECK_LE(ignore_classes_.size(), 1) << "Only ignore class 0 is allowed"; + + // Shader program + GlShader score_shader; + RET_CHECK_CALL( + GlShader::CompileShader(GL_COMPUTE_SHADER, score_src, &score_shader)); + RET_CHECK_CALL( + GpuProgram::CreateWithShader(score_shader, &gpu_data_->score_program)); + // Outputs + size_t scored_boxes_length = num_boxes_ * 2; // score, class + RET_CHECK_CALL(CreateReadWriteShaderStorageBuffer( + scored_boxes_length, &gpu_data_->scored_boxes_buffer)); + // Inputs + size_t raw_scores_length = num_boxes_ * num_classes_; + RET_CHECK_CALL(CreateReadWriteShaderStorageBuffer( + raw_scores_length, &gpu_data_->raw_scores_buffer)); + + return ::mediapipe::OkStatus(); + })); + +#elif defined(__APPLE__) && !TARGET_OS_OSX // iOS + // TODO consolidate Metal and OpenGL shaders via vulkan. + + gpu_data_ = absl::make_unique(); + id device = gpu_helper_.mtlDevice; + + // A shader to decode detection boxes. + std::string decode_src = absl::Substitute( + R"( +#include + +using namespace metal; + +kernel void decodeKernel( + device float* boxes [[ buffer(0) ]], + device float* raw_boxes [[ buffer(1) ]], + device float* raw_anchors [[ buffer(2) ]], + uint2 gid [[ thread_position_in_grid ]]) { + + uint num_coords = uint($0); + int reverse_output_order = int($1); + int apply_exponential = int($2); + int box_coord_offset = int($3); + int num_keypoints = int($4); + int keypt_coord_offset = int($5); + int num_values_per_keypt = int($6); +)", + options_.num_coords(), // box xywh + options_.reverse_output_order() ? 1 : 0, + options_.apply_exponential_on_box_size() ? 1 : 0, + options_.box_coord_offset(), options_.num_keypoints(), + options_.keypoint_coord_offset(), options_.num_values_per_keypoint()); + decode_src += absl::Substitute( + R"( + float4 scale = float4(($0),($1),($2),($3)); +)", + options_.x_scale(), options_.y_scale(), options_.w_scale(), + options_.h_scale()); + decode_src += R"( + uint g_idx = gid.x; + uint box_offset = g_idx * num_coords + uint(box_coord_offset); + uint anchor_offset = g_idx * uint(4); // check kNumCoordsPerBox + + float y_center, x_center, h, w; + + if (reverse_output_order == int(0)) { + y_center = raw_boxes[box_offset + uint(0)]; + x_center = raw_boxes[box_offset + uint(1)]; + h = raw_boxes[box_offset + uint(2)]; + w = raw_boxes[box_offset + uint(3)]; + } else { + x_center = raw_boxes[box_offset + uint(0)]; + y_center = raw_boxes[box_offset + uint(1)]; + w = raw_boxes[box_offset + uint(2)]; + h = raw_boxes[box_offset + uint(3)]; + } + + float anchor_yc = raw_anchors[anchor_offset + uint(0)]; + float anchor_xc = raw_anchors[anchor_offset + uint(1)]; + float anchor_h = raw_anchors[anchor_offset + uint(2)]; + float anchor_w = raw_anchors[anchor_offset + uint(3)]; + + x_center = x_center / scale.x * anchor_w + anchor_xc; + y_center = y_center / scale.y * anchor_h + anchor_yc; + + if (apply_exponential == int(1)) { + h = exp(h / scale.w) * anchor_h; + w = exp(w / scale.z) * anchor_w; + } else { + h = (h / scale.w) * anchor_h; + w = (w / scale.z) * anchor_w; + } + + float ymin = y_center - h / 2.0; + float xmin = x_center - w / 2.0; + float ymax = y_center + h / 2.0; + float xmax = x_center + w / 2.0; + + boxes[box_offset + uint(0)] = ymin; + boxes[box_offset + uint(1)] = xmin; + boxes[box_offset + uint(2)] = ymax; + boxes[box_offset + uint(3)] = xmax; + + if (num_keypoints > int(0)){ + for (int k = 0; k < num_keypoints; ++k) { + int kp_offset = + int(g_idx * num_coords) + keypt_coord_offset + k * num_values_per_keypt; + float kp_y, kp_x; + if (reverse_output_order == int(0)) { + kp_y = raw_boxes[kp_offset + int(0)]; + kp_x = raw_boxes[kp_offset + int(1)]; + } else { + kp_x = raw_boxes[kp_offset + int(0)]; + kp_y = raw_boxes[kp_offset + int(1)]; + } + boxes[kp_offset + int(0)] = kp_x / scale.x * anchor_w + anchor_xc; + boxes[kp_offset + int(1)] = kp_y / scale.y * anchor_h + anchor_yc; + } + } +})"; + + { + // Shader program + NSString* library_source = + [NSString stringWithUTF8String:decode_src.c_str()]; + NSError* error = nil; + id library = [device newLibraryWithSource:library_source + options:nullptr + error:&error]; + RET_CHECK(library != nil) << "Couldn't create shader library " + << [[error localizedDescription] UTF8String]; + id kernel_func = nil; + kernel_func = [library newFunctionWithName:@"decodeKernel"]; + RET_CHECK(kernel_func != nil) << "Couldn't create kernel function."; + gpu_data_->decode_program = + [device newComputePipelineStateWithFunction:kernel_func error:&error]; + RET_CHECK(gpu_data_->decode_program != nil) + << "Couldn't create pipeline state " + << [[error localizedDescription] UTF8String]; + // Outputs + size_t decoded_boxes_length = num_boxes_ * num_coords_ * sizeof(float); + gpu_data_->decoded_boxes_buffer = + [device newBufferWithLength:decoded_boxes_length + options:MTLResourceStorageModeShared]; + // Inputs + size_t raw_boxes_length = num_boxes_ * num_coords_ * sizeof(float); + gpu_data_->raw_boxes_buffer = + [device newBufferWithLength:raw_boxes_length + options:MTLResourceStorageModeShared]; + size_t raw_anchors_length = num_boxes_ * kNumCoordsPerBox * sizeof(float); + gpu_data_->raw_anchors_buffer = + [device newBufferWithLength:raw_anchors_length + options:MTLResourceStorageModeShared]; + } + + // A shader to score detection boxes. + const std::string score_src = absl::Substitute( + R"( +#include + +using namespace metal; + +float optional_sigmoid(float x) { + int apply_sigmoid = int($1); + int apply_clipping_thresh = int($2); + float clipping_thresh = float($3); + if (apply_sigmoid == int(0)) return x; + if (apply_clipping_thresh == int(1)) { + x = clamp(x, -clipping_thresh, clipping_thresh); + } + x = 1.0 / (1.0 + exp(-x)); + return x; +} + +kernel void scoreKernel( + device float* scored_boxes [[ buffer(0) ]], + device float* raw_scores [[ buffer(1) ]], + uint2 tid [[ thread_position_in_threadgroup ]], + uint2 gid [[ thread_position_in_grid ]]) { + + uint num_classes = uint($0); + int apply_sigmoid = int($1); + int apply_clipping_thresh = int($2); + float clipping_thresh = float($3); + int ignore_class_0 = int($4); + + uint g_idx = gid.x; // box idx + uint s_idx = tid.y; // score/class idx + + // load all scores into shared memory + threadgroup float local_scores[$0]; + float score = raw_scores[g_idx * num_classes + s_idx]; + local_scores[s_idx] = optional_sigmoid(score); + threadgroup_barrier(mem_flags::mem_threadgroup); + + // find max score in shared memory + if (s_idx == uint(0)) { + float max_score = -FLT_MAX; + float max_class = -1.0; + for (int i=ignore_class_0; i max_score) { + max_score = local_scores[i]; + max_class = float(i); + } + } + scored_boxes[g_idx * uint(2) + uint(0)] = max_score; + scored_boxes[g_idx * uint(2) + uint(1)] = max_class; + } })", num_classes_, options_.sigmoid_score() ? 1 : 0, options_.has_score_clipping_thresh() ? 1 : 0, @@ -788,42 +1123,44 @@ void main() { : 0, ignore_classes_.size() ? 1 : 0); - // # filter classes supported is hardware dependent. - int max_wg_size; // typically <= 1024 - glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, 1, &max_wg_size); // y-dim - CHECK_LT(num_classes_, max_wg_size) << "# classes must be < " << max_wg_size; // TODO support better filtering. CHECK_LE(ignore_classes_.size(), 1) << "Only ignore class 0 is allowed"; - // Shader program - GlShader score_shader; - status = GlShader::CompileShader(GL_COMPUTE_SHADER, score_src, &score_shader); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } - score_program_ = absl::make_unique(); - status = GlProgram::CreateWithShader(score_shader, score_program_.get()); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } - // Outputs - size_t scored_boxes_length = num_boxes_ * 2; // score, class - scored_boxes_buffer_ = absl::make_unique(); - status = CreateReadWriteShaderStorageBuffer( - scored_boxes_length, scored_boxes_buffer_.get()); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } - // Inputs - size_t raw_scores_length = num_boxes_ * num_classes_; - raw_scores_buffer_ = absl::make_unique(); - status = CreateReadWriteShaderStorageBuffer(raw_scores_length, - raw_scores_buffer_.get()); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); + { + // Shader program + NSString* library_source = + [NSString stringWithUTF8String:score_src.c_str()]; + NSError* error = nil; + id library = [device newLibraryWithSource:library_source + options:nullptr + error:&error]; + RET_CHECK(library != nil) << "Couldn't create shader library " + << [[error localizedDescription] UTF8String]; + id kernel_func = nil; + kernel_func = [library newFunctionWithName:@"scoreKernel"]; + RET_CHECK(kernel_func != nil) << "Couldn't create kernel function."; + gpu_data_->score_program = + [device newComputePipelineStateWithFunction:kernel_func error:&error]; + RET_CHECK(gpu_data_->score_program != nil) + << "Couldn't create pipeline state " + << [[error localizedDescription] UTF8String]; + // Outputs + size_t scored_boxes_length = num_boxes_ * 2 * sizeof(float); // score,class + gpu_data_->scored_boxes_buffer = + [device newBufferWithLength:scored_boxes_length + options:MTLResourceStorageModeShared]; + // Inputs + size_t raw_scores_length = num_boxes_ * num_classes_ * sizeof(float); + gpu_data_->raw_scores_buffer = + [device newBufferWithLength:raw_scores_length + options:MTLResourceStorageModeShared]; + // # filter classes supported is hardware dependent. + int max_wg_size = gpu_data_->score_program.maxTotalThreadsPerThreadgroup; + CHECK_LT(num_classes_, max_wg_size) << "# classes must be <" << max_wg_size; } -#endif // defined(__ANDROID__) +#endif // __ANDROID__ or iOS + return ::mediapipe::OkStatus(); } diff --git a/mediapipe/calculators/tflite/tflite_tensors_to_landmarks_calculator.cc b/mediapipe/calculators/tflite/tflite_tensors_to_landmarks_calculator.cc index 3b938a1cd..1d646e4a3 100644 --- a/mediapipe/calculators/tflite/tflite_tensors_to_landmarks_calculator.cc +++ b/mediapipe/calculators/tflite/tflite_tensors_to_landmarks_calculator.cc @@ -96,7 +96,8 @@ REGISTER_CALCULATOR(TfLiteTensorsToLandmarksCalculator); options_.has_input_image_width()) << "Must provide input with/height for getting normalized landmarks."; } - if (cc->Outputs().HasTag("LANDMARKS") && options_.flip_vertically()) { + if (cc->Outputs().HasTag("LANDMARKS") && + (options_.flip_vertically() || options_.flip_horizontally())) { RET_CHECK(options_.has_input_image_height() && options_.has_input_image_width()) << "Must provide input with/height for using flip_vertically option " @@ -133,7 +134,12 @@ REGISTER_CALCULATOR(TfLiteTensorsToLandmarksCalculator); for (int ld = 0; ld < num_landmarks_; ++ld) { const int offset = ld * num_dimensions; Landmark landmark; - landmark.set_x(raw_landmarks[offset]); + + if (options_.flip_horizontally()) { + landmark.set_x(options_.input_image_width() - raw_landmarks[offset]); + } else { + landmark.set_x(raw_landmarks[offset]); + } if (num_dimensions > 1) { if (options_.flip_vertically()) { landmark.set_y(options_.input_image_height() - diff --git a/mediapipe/calculators/tflite/tflite_tensors_to_landmarks_calculator.proto b/mediapipe/calculators/tflite/tflite_tensors_to_landmarks_calculator.proto index 5f37e6238..3b6716c9c 100644 --- a/mediapipe/calculators/tflite/tflite_tensors_to_landmarks_calculator.proto +++ b/mediapipe/calculators/tflite/tflite_tensors_to_landmarks_calculator.proto @@ -40,6 +40,12 @@ message TfLiteTensorsToLandmarksCalculatorOptions { // representation has a bottom-left origin (e.g., in OpenGL). optional bool flip_vertically = 4 [default = false]; + // Whether the detection coordinates from the input tensors should be flipped + // horizontally (along the x-direction). This is useful, for example, when the + // input image is horizontally flipped in ImageTransformationCalculator + // beforehand. + optional bool flip_horizontally = 6 [default = false]; + // A value that z values should be divided by. optional float normalize_z = 5 [default = 1.0]; } diff --git a/mediapipe/calculators/tflite/tflite_tensors_to_segmentation_calculator.cc b/mediapipe/calculators/tflite/tflite_tensors_to_segmentation_calculator.cc index 20aca8e30..16805a066 100644 --- a/mediapipe/calculators/tflite/tflite_tensors_to_segmentation_calculator.cc +++ b/mediapipe/calculators/tflite/tflite_tensors_to_segmentation_calculator.cc @@ -17,6 +17,7 @@ #include "absl/strings/str_format.h" #include "absl/types/span.h" #include "mediapipe/calculators/tflite/tflite_tensors_to_segmentation_calculator.pb.h" +#include "mediapipe/calculators/tflite/util.h" #include "mediapipe/framework/calculator_context.h" #include "mediapipe/framework/calculator_framework.h" #include "mediapipe/framework/formats/image_frame.h" @@ -27,7 +28,7 @@ #include "mediapipe/util/resource_util.h" #include "tensorflow/lite/interpreter.h" -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) #include "mediapipe/gpu/gl_calculator_helper.h" #include "mediapipe/gpu/gl_simple_shaders.h" #include "mediapipe/gpu/shader_util.h" @@ -36,7 +37,7 @@ #include "tensorflow/lite/delegates/gpu/gl/gl_shader.h" #include "tensorflow/lite/delegates/gpu/gl/gl_texture.h" #include "tensorflow/lite/delegates/gpu/gl_delegate.h" -#endif // __ANDROID__ +#endif // !MEDIAPIPE_DISABLE_GPU namespace { constexpr int kWorkgroupSize = 8; // Block size for GPU shader. @@ -52,12 +53,14 @@ float Clamp(float val, float min, float max) { namespace mediapipe { -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) +using ::tflite::gpu::gl::CopyBuffer; +using ::tflite::gpu::gl::CreateReadWriteRgbaImageTexture; using ::tflite::gpu::gl::CreateReadWriteShaderStorageBuffer; using ::tflite::gpu::gl::GlBuffer; using ::tflite::gpu::gl::GlProgram; using ::tflite::gpu::gl::GlShader; -#endif // __ANDROID__ +#endif // !MEDIAPIPE_DISABLE_GPU // Converts TFLite tensors from a tflite segmentation model to an image mask. // @@ -126,13 +129,13 @@ class TfLiteTensorsToSegmentationCalculator : public CalculatorBase { int tensor_channels_ = 0; bool use_gpu_ = false; -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) mediapipe::GlCalculatorHelper gpu_helper_; std::unique_ptr mask_program_with_prev_; std::unique_ptr mask_program_no_prev_; std::unique_ptr tensor_buffer_; GLuint upsample_program_; -#endif // __ANDROID__ +#endif // !MEDIAPIPE_DISABLE_GPU }; REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); @@ -142,6 +145,8 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); RET_CHECK(!cc->Inputs().GetTags().empty()); RET_CHECK(!cc->Outputs().GetTags().empty()); + bool use_gpu = false; + // Inputs CPU. if (cc->Inputs().HasTag("TENSORS")) { cc->Inputs().Tag("TENSORS").Set>(); @@ -154,32 +159,37 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); } // Inputs GPU. -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) if (cc->Inputs().HasTag("TENSORS_GPU")) { cc->Inputs().Tag("TENSORS_GPU").Set>(); + use_gpu |= true; } if (cc->Inputs().HasTag("PREV_MASK_GPU")) { cc->Inputs().Tag("PREV_MASK_GPU").Set(); + use_gpu |= true; } if (cc->Inputs().HasTag("REFERENCE_IMAGE_GPU")) { cc->Inputs().Tag("REFERENCE_IMAGE_GPU").Set(); + use_gpu |= true; } -#endif // __ANDROID__ +#endif // !MEDIAPIPE_DISABLE_GPU // Outputs. if (cc->Outputs().HasTag("MASK")) { cc->Outputs().Tag("MASK").Set(); } -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) if (cc->Outputs().HasTag("MASK_GPU")) { cc->Outputs().Tag("MASK_GPU").Set(); + use_gpu |= true; } -#endif // __ANDROID__ - -#if defined(__ANDROID__) - MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); -#endif // __ANDROID__ +#endif // !MEDIAPIPE_DISABLE_GPU + if (use_gpu) { +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) + MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); +#endif // !MEDIAPIPE_DISABLE_GPU + } return ::mediapipe::OkStatus(); } @@ -189,24 +199,23 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); if (cc->Inputs().HasTag("TENSORS_GPU")) { use_gpu_ = true; -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) MP_RETURN_IF_ERROR(gpu_helper_.Open(cc)); -#endif // __ANDROID__ +#endif // !MEDIAPIPE_DISABLE_GPU } MP_RETURN_IF_ERROR(LoadOptions(cc)); if (use_gpu_) { -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) MP_RETURN_IF_ERROR( gpu_helper_.RunInGlContext([this, cc]() -> ::mediapipe::Status { MP_RETURN_IF_ERROR(InitGpu(cc)); return ::mediapipe::OkStatus(); })); #else - RET_CHECK_FAIL() - << "GPU processing on non-Android devices is not supported yet."; -#endif // __ANDROID__ + RET_CHECK_FAIL() << "GPU processing not enabled."; +#endif // !MEDIAPIPE_DISABLE_GPU } return ::mediapipe::OkStatus(); @@ -215,13 +224,13 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); ::mediapipe::Status TfLiteTensorsToSegmentationCalculator::Process( CalculatorContext* cc) { if (use_gpu_) { -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) MP_RETURN_IF_ERROR( gpu_helper_.RunInGlContext([this, cc]() -> ::mediapipe::Status { MP_RETURN_IF_ERROR(ProcessGpu(cc)); return ::mediapipe::OkStatus(); })); -#endif // __ANDROID__ +#endif // !MEDIAPIPE_DISABLE_GPU } else { MP_RETURN_IF_ERROR(ProcessCpu(cc)); } @@ -231,7 +240,7 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); ::mediapipe::Status TfLiteTensorsToSegmentationCalculator::Close( CalculatorContext* cc) { -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) gpu_helper_.RunInGlContext([this] { if (upsample_program_) glDeleteProgram(upsample_program_); upsample_program_ = 0; @@ -239,7 +248,7 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); mask_program_no_prev_.reset(); tensor_buffer_.reset(); }); -#endif // __ANDROID__ +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } @@ -358,7 +367,7 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); if (cc->Inputs().Tag("TENSORS_GPU").IsEmpty()) { return ::mediapipe::OkStatus(); } -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) // Get input streams. const auto& input_tensors = cc->Inputs().Tag("TENSORS_GPU").Get>(); @@ -379,9 +388,9 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); // Create initial working mask texture. ::tflite::gpu::gl::GlTexture small_mask_texture; - ::tflite::gpu::gl::CreateReadWriteRgbaImageTexture( + RET_CHECK_CALL(CreateReadWriteRgbaImageTexture( tflite::gpu::DataType::UINT8, // GL_RGBA8 - {tensor_width_, tensor_height_}, &small_mask_texture); + {tensor_width_, tensor_height_}, &small_mask_texture)); // Get input previous mask. auto input_mask_texture = has_prev_mask @@ -389,7 +398,7 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); : mediapipe::GlTexture(); // Copy input tensor. - tflite::gpu::gl::CopyBuffer(input_tensors[0], *tensor_buffer_); + RET_CHECK_CALL(CopyBuffer(input_tensors[0], *tensor_buffer_)); // Run shader, process mask tensor. // Run softmax over tensor output and blend with previous mask. @@ -397,18 +406,18 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); const int output_index = 0; glBindImageTexture(output_index, small_mask_texture.id(), 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8); - tensor_buffer_->BindToIndex(2); + RET_CHECK_CALL(tensor_buffer_->BindToIndex(2)); const tflite::gpu::uint3 workgroups = { NumGroups(tensor_width_, kWorkgroupSize), NumGroups(tensor_height_, kWorkgroupSize), 1}; if (!has_prev_mask) { - mask_program_no_prev_->Dispatch(workgroups); + RET_CHECK_CALL(mask_program_no_prev_->Dispatch(workgroups)); } else { glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, input_mask_texture.name()); - mask_program_with_prev_->Dispatch(workgroups); + RET_CHECK_CALL(mask_program_with_prev_->Dispatch(workgroups)); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, 0); } @@ -438,13 +447,13 @@ REGISTER_CALCULATOR(TfLiteTensorsToSegmentationCalculator); // Cleanup input_mask_texture.Release(); output_texture.Release(); -#endif // __ANDROID__ +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } void TfLiteTensorsToSegmentationCalculator::GlRender() { -#if defined(__ANDROID__) +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) static const GLfloat square_vertices[] = { -1.0f, -1.0f, // bottom left 1.0f, -1.0f, // bottom right @@ -492,7 +501,7 @@ void TfLiteTensorsToSegmentationCalculator::GlRender() { glBindVertexArray(0); glDeleteVertexArrays(1, &vao); glDeleteBuffers(2, vbo); -#endif // __ANDROID__ +#endif // !MEDIAPIPE_DISABLE_GPU } ::mediapipe::Status TfLiteTensorsToSegmentationCalculator::LoadOptions( @@ -516,14 +525,15 @@ void TfLiteTensorsToSegmentationCalculator::GlRender() { ::mediapipe::Status TfLiteTensorsToSegmentationCalculator::InitGpu( CalculatorContext* cc) { -#if defined(__ANDROID__) - - // A shader to process a segmentation tensor into an output mask, - // and use an optional previous mask as input. - // Currently uses 4 channels for output, - // and sets both R and A channels as mask value. - const std::string shader_src_template = - R"( #version 310 es +#if !defined(MEDIAPIPE_DISABLE_GPU) && !defined(__APPLE__) + MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext([this]() + -> ::mediapipe::Status { + // A shader to process a segmentation tensor into an output mask, + // and use an optional previous mask as input. + // Currently uses 4 channels for output, + // and sets both R and A channels as mask value. + const std::string shader_src_template = + R"( #version 310 es layout(local_size_x = $0, local_size_y = $0, local_size_z = 1) in; @@ -589,76 +599,60 @@ void main() { imageStore(output_texture, output_coordinate, out_value); })"; - const std::string shader_src_no_previous = absl::Substitute( - shader_src_template, kWorkgroupSize, options_.output_layer_index(), - options_.combine_with_previous_ratio(), "", - options_.flip_vertically() ? "out_height - gid.y - 1" : "gid.y"); - const std::string shader_src_with_previous = absl::Substitute( - shader_src_template, kWorkgroupSize, options_.output_layer_index(), - options_.combine_with_previous_ratio(), "#define READ_PREVIOUS", - options_.flip_vertically() ? "out_height - gid.y - 1" : "gid.y"); + const std::string shader_src_no_previous = absl::Substitute( + shader_src_template, kWorkgroupSize, options_.output_layer_index(), + options_.combine_with_previous_ratio(), "", + options_.flip_vertically() ? "out_height - gid.y - 1" : "gid.y"); + const std::string shader_src_with_previous = absl::Substitute( + shader_src_template, kWorkgroupSize, options_.output_layer_index(), + options_.combine_with_previous_ratio(), "#define READ_PREVIOUS", + options_.flip_vertically() ? "out_height - gid.y - 1" : "gid.y"); - auto status = ::tflite::gpu::OkStatus(); + // Shader programs. + GlShader shader_without_previous; + RET_CHECK_CALL(GlShader::CompileShader( + GL_COMPUTE_SHADER, shader_src_no_previous, &shader_without_previous)); + mask_program_no_prev_ = absl::make_unique(); + RET_CHECK_CALL(GlProgram::CreateWithShader(shader_without_previous, + mask_program_no_prev_.get())); + GlShader shader_with_previous; + RET_CHECK_CALL(GlShader::CompileShader( + GL_COMPUTE_SHADER, shader_src_with_previous, &shader_with_previous)); + mask_program_with_prev_ = absl::make_unique(); + RET_CHECK_CALL(GlProgram::CreateWithShader(shader_with_previous, + mask_program_with_prev_.get())); - // Shader programs. - GlShader shader_without_previous; - status = GlShader::CompileShader(GL_COMPUTE_SHADER, shader_src_no_previous, - &shader_without_previous); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } - mask_program_no_prev_ = absl::make_unique(); - status = GlProgram::CreateWithShader(shader_without_previous, - mask_program_no_prev_.get()); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } - GlShader shader_with_previous; - status = GlShader::CompileShader(GL_COMPUTE_SHADER, shader_src_with_previous, - &shader_with_previous); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } - mask_program_with_prev_ = absl::make_unique(); - status = GlProgram::CreateWithShader(shader_with_previous, - mask_program_with_prev_.get()); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } + // Buffer storage for input tensor. + size_t tensor_length = tensor_width_ * tensor_height_ * tensor_channels_; + tensor_buffer_ = absl::make_unique(); + RET_CHECK_CALL(CreateReadWriteShaderStorageBuffer( + tensor_length, tensor_buffer_.get())); - // Buffer storage for input tensor. - size_t tensor_length = tensor_width_ * tensor_height_ * tensor_channels_; - tensor_buffer_ = absl::make_unique(); - status = CreateReadWriteShaderStorageBuffer(tensor_length, - tensor_buffer_.get()); - if (!status.ok()) { - return ::mediapipe::InternalError(status.error_message()); - } + // Parameters. + glUseProgram(mask_program_with_prev_->id()); + glUniform2i(glGetUniformLocation(mask_program_with_prev_->id(), "out_size"), + tensor_width_, tensor_height_); + glUniform1i( + glGetUniformLocation(mask_program_with_prev_->id(), "input_texture"), + 1); + glUseProgram(mask_program_no_prev_->id()); + glUniform2i(glGetUniformLocation(mask_program_no_prev_->id(), "out_size"), + tensor_width_, tensor_height_); + glUniform1i( + glGetUniformLocation(mask_program_no_prev_->id(), "input_texture"), 1); - // Parameters. - glUseProgram(mask_program_with_prev_->id()); - glUniform2i(glGetUniformLocation(mask_program_with_prev_->id(), "out_size"), - tensor_width_, tensor_height_); - glUniform1i( - glGetUniformLocation(mask_program_with_prev_->id(), "input_texture"), 1); - glUseProgram(mask_program_no_prev_->id()); - glUniform2i(glGetUniformLocation(mask_program_no_prev_->id(), "out_size"), - tensor_width_, tensor_height_); - glUniform1i( - glGetUniformLocation(mask_program_no_prev_->id(), "input_texture"), 1); + // Vertex shader attributes. + const GLint attr_location[NUM_ATTRIBUTES] = { + ATTRIB_VERTEX, + ATTRIB_TEXTURE_POSITION, + }; + const GLchar* attr_name[NUM_ATTRIBUTES] = { + "position", + "texture_coordinate", + }; - // Vertex shader attributes. - const GLint attr_location[NUM_ATTRIBUTES] = { - ATTRIB_VERTEX, - ATTRIB_TEXTURE_POSITION, - }; - const GLchar* attr_name[NUM_ATTRIBUTES] = { - "position", - "texture_coordinate", - }; - - // Simple pass-through shader, used for hardware upsampling. - std::string upsample_shader_base = R"( + // Simple pass-through shader, used for hardware upsampling. + std::string upsample_shader_base = R"( #if __VERSION__ < 130 #define in varying #endif // __VERSION__ < 130 @@ -683,16 +677,19 @@ void main() { } )"; - // Program - mediapipe::GlhCreateProgram(mediapipe::kBasicVertexShader, - upsample_shader_base.c_str(), NUM_ATTRIBUTES, - &attr_name[0], attr_location, &upsample_program_); - RET_CHECK(upsample_program_) << "Problem initializing the program."; + // Program + mediapipe::GlhCreateProgram( + mediapipe::kBasicVertexShader, upsample_shader_base.c_str(), + NUM_ATTRIBUTES, &attr_name[0], attr_location, &upsample_program_); + RET_CHECK(upsample_program_) << "Problem initializing the program."; - // Parameters - glUseProgram(upsample_program_); - glUniform1i(glGetUniformLocation(upsample_program_, "input_data"), 1); -#endif // __ANDROID__ + // Parameters + glUseProgram(upsample_program_); + glUniform1i(glGetUniformLocation(upsample_program_, "input_data"), 1); + + return ::mediapipe::OkStatus(); + })); +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } diff --git a/mediapipe/calculators/tflite/util.h b/mediapipe/calculators/tflite/util.h new file mode 100644 index 000000000..53e0927af --- /dev/null +++ b/mediapipe/calculators/tflite/util.h @@ -0,0 +1,25 @@ +// 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_TFLITE_UTIL_H_ +#define MEDIAPIPE_CALCULATORS_TFLITE_UTIL_H_ + +#define RET_CHECK_CALL(call) \ + do { \ + const auto status = (call); \ + if (ABSL_PREDICT_FALSE(!status.ok())) \ + return ::mediapipe::InternalError(status.error_message()); \ + } while (0); + +#endif // MEDIAPIPE_CALCULATORS_TFLITE_UTIL_H_ diff --git a/mediapipe/calculators/util/BUILD b/mediapipe/calculators/util/BUILD index 3dde96aee..7bd06fe97 100644 --- a/mediapipe/calculators/util/BUILD +++ b/mediapipe/calculators/util/BUILD @@ -235,19 +235,13 @@ cc_library( "//mediapipe/framework/port:vector", "//mediapipe/util:annotation_renderer", ] + select({ - "//mediapipe:android": [ + "//mediapipe/gpu:disable_gpu": [], + "//conditions:default": [ "//mediapipe/gpu:gl_calculator_helper", "//mediapipe/gpu:gl_simple_shaders", "//mediapipe/gpu:gpu_buffer", "//mediapipe/gpu:shader_util", ], - "//mediapipe:ios": [ - "//mediapipe/gpu:gl_calculator_helper", - "//mediapipe/gpu:gl_simple_shaders", - "//mediapipe/gpu:gpu_buffer", - "//mediapipe/gpu:shader_util", - ], - "//conditions:default": [], }), alwayslink = 1, ) @@ -694,3 +688,65 @@ cc_test( "//mediapipe/framework/tool:validate_type", ], ) + +proto_library( + name = "top_k_scores_calculator_proto", + srcs = ["top_k_scores_calculator.proto"], + visibility = ["//visibility:public"], + deps = [ + "//mediapipe/framework:calculator_proto", + ], +) + +mediapipe_cc_proto_library( + name = "top_k_scores_calculator_cc_proto", + srcs = ["top_k_scores_calculator.proto"], + cc_deps = [ + "//mediapipe/framework:calculator_cc_proto", + ], + visibility = ["//visibility:public"], + deps = [":top_k_scores_calculator_proto"], +) + +cc_library( + name = "top_k_scores_calculator", + srcs = ["top_k_scores_calculator.cc"], + visibility = ["//visibility:public"], + deps = [ + ":top_k_scores_calculator_cc_proto", + "//mediapipe/framework/port:ret_check", + "//mediapipe/framework/port:status", + "//mediapipe/framework/port:statusor", + "//mediapipe/framework:calculator_framework", + "//mediapipe/util:resource_util", + ] + select({ + "//mediapipe:android": [ + "//mediapipe/util/android/file/base", + ], + "//mediapipe:apple": [ + "//mediapipe/util/android/file/base", + ], + "//mediapipe:macos": [ + "//mediapipe/framework/port:file_helpers", + ], + "//conditions:default": [ + "//mediapipe/framework/port:file_helpers", + ], + }), + alwayslink = 1, +) + +cc_test( + name = "top_k_scores_calculator_test", + srcs = ["top_k_scores_calculator_test.cc"], + deps = [ + ":top_k_scores_calculator", + "//mediapipe/framework:calculator_framework", + "//mediapipe/framework:calculator_runner", + "//mediapipe/framework:packet", + "//mediapipe/framework/deps:message_matchers", + "//mediapipe/framework/port:gtest_main", + "//mediapipe/framework/port:parse_text_proto", + "//mediapipe/framework/port:status", + ], +) diff --git a/mediapipe/calculators/util/annotation_overlay_calculator.cc b/mediapipe/calculators/util/annotation_overlay_calculator.cc index ee21302ef..5f5c53582 100644 --- a/mediapipe/calculators/util/annotation_overlay_calculator.cc +++ b/mediapipe/calculators/util/annotation_overlay_calculator.cc @@ -27,12 +27,12 @@ #include "mediapipe/util/annotation_renderer.h" #include "mediapipe/util/color.pb.h" -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) #include "mediapipe/gpu/gl_calculator_helper.h" #include "mediapipe/gpu/gl_simple_shaders.h" #include "mediapipe/gpu/gpu_buffer.h" #include "mediapipe/gpu/shader_util.h" -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU namespace mediapipe { @@ -146,13 +146,13 @@ class AnnotationOverlayCalculator : public CalculatorBase { bool use_gpu_ = false; bool gpu_initialized_ = false; -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) mediapipe::GlCalculatorHelper gpu_helper_; GLuint program_ = 0; GLuint image_mat_tex_ = 0; // Overlay drawing image for GPU. int width_ = 0; int height_ = 0; -#endif // __ANDROID__ or iOS +#endif // MEDIAPIPE_DISABLE_GPU }; REGISTER_CALCULATOR(AnnotationOverlayCalculator); @@ -160,6 +160,8 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); CalculatorContract* cc) { CHECK_GE(cc->Inputs().NumEntries(), 1); + bool use_gpu = false; + if (cc->Inputs().HasTag(kInputFrameTag) && cc->Inputs().HasTag(kInputFrameTagGpu)) { return ::mediapipe::InternalError("Cannot have multiple input images."); @@ -173,12 +175,13 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); int num_render_streams = cc->Inputs().NumEntries(); // Input image to render onto copy of. -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) if (cc->Inputs().HasTag(kInputFrameTagGpu)) { cc->Inputs().Tag(kInputFrameTagGpu).Set(); num_render_streams = cc->Inputs().NumEntries() - 1; + use_gpu |= true; } -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU if (cc->Inputs().HasTag(kInputFrameTag)) { cc->Inputs().Tag(kInputFrameTag).Set(); num_render_streams = cc->Inputs().NumEntries() - 1; @@ -190,18 +193,21 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); } // Rendered image. -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) if (cc->Outputs().HasTag(kOutputFrameTagGpu)) { cc->Outputs().Tag(kOutputFrameTagGpu).Set(); + use_gpu |= true; } -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU if (cc->Outputs().HasTag(kOutputFrameTag)) { cc->Outputs().Tag(kOutputFrameTag).Set(); } -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) - MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); -#endif // __ANDROID__ or iOS + if (use_gpu) { +#if !defined(MEDIAPIPE_DISABLE_GPU) + MP_RETURN_IF_ERROR(mediapipe::GlCalculatorHelper::UpdateContract(cc)); +#endif // !MEDIAPIPE_DISABLE_GPU + } return ::mediapipe::OkStatus(); } @@ -212,11 +218,11 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); options_ = cc->Options(); if (cc->Inputs().HasTag(kInputFrameTagGpu) && cc->Outputs().HasTag(kOutputFrameTagGpu)) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) use_gpu_ = true; #else - RET_CHECK_FAIL() << "GPU processing is for Android and iOS only."; -#endif // __ANDROID__ or iOS + RET_CHECK_FAIL() << "GPU processing not enabled."; +#endif // !MEDIAPIPE_DISABLE_GPU } if (cc->Inputs().HasTag(kInputFrameTagGpu) || @@ -246,9 +252,9 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); } if (use_gpu_) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) MP_RETURN_IF_ERROR(gpu_helper_.Open(cc)); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU } return ::mediapipe::OkStatus(); @@ -260,7 +266,7 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); std::unique_ptr image_mat; ImageFormat::Format target_format; if (use_gpu_) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) if (!gpu_initialized_) { MP_RETURN_IF_ERROR( gpu_helper_.RunInGlContext([this, cc]() -> ::mediapipe::Status { @@ -269,7 +275,7 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); })); gpu_initialized_ = true; } -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU MP_RETURN_IF_ERROR(CreateRenderTargetGpu(cc, image_mat)); } else { MP_RETURN_IF_ERROR(CreateRenderTargetCpu(cc, image_mat, &target_format)); @@ -288,7 +294,7 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); } if (use_gpu_) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) // Overlay rendered image in OpenGL, onto a copy of input. uchar* image_mat_ptr = image_mat->data; MP_RETURN_IF_ERROR(gpu_helper_.RunInGlContext( @@ -296,7 +302,7 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); MP_RETURN_IF_ERROR(RenderToGpu(cc, image_mat_ptr)); return ::mediapipe::OkStatus(); })); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU } else { // Copy the rendered image to output. uchar* image_mat_ptr = image_mat->data; @@ -307,14 +313,14 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); } ::mediapipe::Status AnnotationOverlayCalculator::Close(CalculatorContext* cc) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) gpu_helper_.RunInGlContext([this] { if (program_) glDeleteProgram(program_); program_ = 0; if (image_mat_tex_) glDeleteTextures(1, &image_mat_tex_); image_mat_tex_ = 0; }); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } @@ -325,7 +331,7 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); auto output_frame = absl::make_unique( target_format, renderer_->GetImageWidth(), renderer_->GetImageHeight()); -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) output_frame->CopyPixelData(target_format, renderer_->GetImageWidth(), renderer_->GetImageHeight(), data_image, ImageFrame::kGlDefaultAlignmentBoundary); @@ -333,7 +339,7 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); output_frame->CopyPixelData(target_format, renderer_->GetImageWidth(), renderer_->GetImageHeight(), data_image, ImageFrame::kDefaultAlignmentBoundary); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU cc->Outputs() .Tag(kOutputFrameTag) @@ -344,7 +350,7 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); ::mediapipe::Status AnnotationOverlayCalculator::RenderToGpu( CalculatorContext* cc, uchar* overlay_image) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) // Source and destination textures. const auto& input_frame = cc->Inputs().Tag(kInputFrameTagGpu).Get(); @@ -390,7 +396,7 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); // Cleanup input_texture.Release(); output_texture.Release(); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } @@ -451,15 +457,16 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); ::mediapipe::Status AnnotationOverlayCalculator::CreateRenderTargetGpu( CalculatorContext* cc, std::unique_ptr& image_mat) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) if (image_frame_available_) { const auto& input_frame = cc->Inputs().Tag(kInputFrameTagGpu).Get(); const mediapipe::ImageFormat::Format format = mediapipe::ImageFormatForGpuBufferFormat(input_frame.format()); - if (format != mediapipe::ImageFormat::SRGBA) - RET_CHECK_FAIL() << "Unsupported GPU input format."; + if (format != mediapipe::ImageFormat::SRGBA && + format != mediapipe::ImageFormat::SRGB) + RET_CHECK_FAIL() << "Unsupported GPU input format: " << format; image_mat = absl::make_unique( height_, width_, CV_8UC3, @@ -471,14 +478,14 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); cv::Scalar(options_.canvas_color().r(), options_.canvas_color().g(), options_.canvas_color().b())); } -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } ::mediapipe::Status AnnotationOverlayCalculator::GlRender( CalculatorContext* cc) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) static const GLfloat square_vertices[] = { -1.0f, -1.0f, // bottom left 1.0f, -1.0f, // bottom right @@ -526,14 +533,14 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); glBindVertexArray(0); glDeleteVertexArrays(1, &vao); glDeleteBuffers(2, vbo); -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } ::mediapipe::Status AnnotationOverlayCalculator::GlSetup( CalculatorContext* cc) { -#if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_OSX) +#if !defined(MEDIAPIPE_DISABLE_GPU) const GLint attr_location[NUM_ATTRIBUTES] = { ATTRIB_VERTEX, ATTRIB_TEXTURE_POSITION, @@ -609,7 +616,7 @@ REGISTER_CALCULATOR(AnnotationOverlayCalculator); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glBindTexture(GL_TEXTURE_2D, 0); } -#endif // __ANDROID__ or iOS +#endif // !MEDIAPIPE_DISABLE_GPU return ::mediapipe::OkStatus(); } diff --git a/mediapipe/calculators/util/rect_to_render_data_calculator.cc b/mediapipe/calculators/util/rect_to_render_data_calculator.cc index 12cce1fa2..24db78a88 100644 --- a/mediapipe/calculators/util/rect_to_render_data_calculator.cc +++ b/mediapipe/calculators/util/rect_to_render_data_calculator.cc @@ -23,10 +23,25 @@ namespace mediapipe { namespace { -constexpr char kNormalizedRectTag[] = "NORM_RECT"; +constexpr char kNormRectTag[] = "NORM_RECT"; constexpr char kRectTag[] = "RECT"; +constexpr char kNormRectsTag[] = "NORM_RECTS"; +constexpr char kRectsTag[] = "RECTS"; constexpr char kRenderDataTag[] = "RENDER_DATA"; +RenderAnnotation::Rectangle* NewRect( + const RectToRenderDataCalculatorOptions& options, RenderData* render_data) { + auto* annotation = render_data->add_render_annotations(); + annotation->mutable_color()->set_r(options.color().r()); + annotation->mutable_color()->set_g(options.color().g()); + annotation->mutable_color()->set_b(options.color().b()); + annotation->set_thickness(options.thickness()); + + return options.filled() + ? annotation->mutable_filled_rectangle()->mutable_rectangle() + : annotation->mutable_rectangle(); +} + void SetRect(bool normalized, double xmin, double ymin, double width, double height, double rotation, RenderAnnotation::Rectangle* rect) { @@ -51,6 +66,8 @@ void SetRect(bool normalized, double xmin, double ymin, double width, // One of the following: // NORM_RECT: A NormalizedRect // RECT: A Rect +// NORM_RECTS: An std::vector +// RECTS: An std::vector // // Output: // RENDER_DATA: A RenderData @@ -83,16 +100,27 @@ REGISTER_CALCULATOR(RectToRenderDataCalculator); ::mediapipe::Status RectToRenderDataCalculator::GetContract( CalculatorContract* cc) { - RET_CHECK(cc->Inputs().HasTag(kNormalizedRectTag) ^ - cc->Inputs().HasTag(kRectTag)); + RET_CHECK_EQ((cc->Inputs().HasTag(kNormRectTag) ? 1 : 0) + + (cc->Inputs().HasTag(kRectTag) ? 1 : 0) + + (cc->Inputs().HasTag(kNormRectsTag) ? 1 : 0) + + (cc->Inputs().HasTag(kRectsTag) ? 1 : 0), + 1) + << "Exactly one of NORM_RECT, RECT, NORM_RECTS or RECTS input stream " + "should be provided."; RET_CHECK(cc->Outputs().HasTag(kRenderDataTag)); - if (cc->Inputs().HasTag(kNormalizedRectTag)) { - cc->Inputs().Tag(kNormalizedRectTag).Set(); + if (cc->Inputs().HasTag(kNormRectTag)) { + cc->Inputs().Tag(kNormRectTag).Set(); } if (cc->Inputs().HasTag(kRectTag)) { cc->Inputs().Tag(kRectTag).Set(); } + if (cc->Inputs().HasTag(kNormRectsTag)) { + cc->Inputs().Tag(kNormRectsTag).Set>(); + } + if (cc->Inputs().HasTag(kRectsTag)) { + cc->Inputs().Tag(kRectsTag).Set>(); + } cc->Outputs().Tag(kRenderDataTag).Set(); return ::mediapipe::OkStatus(); @@ -108,31 +136,43 @@ REGISTER_CALCULATOR(RectToRenderDataCalculator); ::mediapipe::Status RectToRenderDataCalculator::Process(CalculatorContext* cc) { auto render_data = absl::make_unique(); - auto* annotation = render_data->add_render_annotations(); - annotation->mutable_color()->set_r(options_.color().r()); - annotation->mutable_color()->set_g(options_.color().g()); - annotation->mutable_color()->set_b(options_.color().b()); - annotation->set_thickness(options_.thickness()); - auto* rectangle = - options_.filled() - ? annotation->mutable_filled_rectangle()->mutable_rectangle() - : annotation->mutable_rectangle(); - - if (cc->Inputs().HasTag(kNormalizedRectTag) && - !cc->Inputs().Tag(kNormalizedRectTag).IsEmpty()) { - const auto& rect = - cc->Inputs().Tag(kNormalizedRectTag).Get(); + if (cc->Inputs().HasTag(kNormRectTag) && + !cc->Inputs().Tag(kNormRectTag).IsEmpty()) { + const auto& rect = cc->Inputs().Tag(kNormRectTag).Get(); + auto* rectangle = NewRect(options_, render_data.get()); SetRect(/*normalized=*/true, rect.x_center() - rect.width() / 2.f, rect.y_center() - rect.height() / 2.f, rect.width(), rect.height(), rect.rotation(), rectangle); } if (cc->Inputs().HasTag(kRectTag) && !cc->Inputs().Tag(kRectTag).IsEmpty()) { const auto& rect = cc->Inputs().Tag(kRectTag).Get(); + auto* rectangle = NewRect(options_, render_data.get()); SetRect(/*normalized=*/false, rect.x_center() - rect.width() / 2.f, rect.y_center() - rect.height() / 2.f, rect.width(), rect.height(), rect.rotation(), rectangle); } + if (cc->Inputs().HasTag(kNormRectsTag) && + !cc->Inputs().Tag(kNormRectsTag).IsEmpty()) { + const auto& rects = + cc->Inputs().Tag(kNormRectsTag).Get>(); + for (auto& rect : rects) { + auto* rectangle = NewRect(options_, render_data.get()); + SetRect(/*normalized=*/true, rect.x_center() - rect.width() / 2.f, + rect.y_center() - rect.height() / 2.f, rect.width(), + rect.height(), rect.rotation(), rectangle); + } + } + if (cc->Inputs().HasTag(kRectsTag) && + !cc->Inputs().Tag(kRectsTag).IsEmpty()) { + const auto& rects = cc->Inputs().Tag(kRectsTag).Get>(); + for (auto& rect : rects) { + auto* rectangle = NewRect(options_, render_data.get()); + SetRect(/*normalized=*/false, rect.x_center() - rect.width() / 2.f, + rect.y_center() - rect.height() / 2.f, rect.width(), + rect.height(), rect.rotation(), rectangle); + } + } cc->Outputs() .Tag(kRenderDataTag) diff --git a/mediapipe/calculators/util/top_k_scores_calculator.cc b/mediapipe/calculators/util/top_k_scores_calculator.cc new file mode 100644 index 000000000..18f2eec62 --- /dev/null +++ b/mediapipe/calculators/util/top_k_scores_calculator.cc @@ -0,0 +1,194 @@ +// 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 +#include +#include + +#include "mediapipe/calculators/util/top_k_scores_calculator.pb.h" +#include "mediapipe/framework/calculator_framework.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) +#include "mediapipe/util/android/file/base/file.h" +#include "mediapipe/util/android/file/base/helpers.h" +#else +#include "mediapipe/framework/port/file_helpers.h" +#endif + +namespace mediapipe { +// A calculator that takes a vector of scores and returns the indexes, scores, +// labels of the top k elements. +// +// Usage example: +// node { +// calculator: "TopKScoresCalculator" +// input_stream: "SCORES:score_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" +// options: { +// [mediapipe.TopKScoresCalculatorOptions.ext] { +// top_k: 5 +// threshold: 0.1 +// label_map_path: "/path/to/label/map" +// } +// } +// } +class TopKScoresCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc); + + ::mediapipe::Status Open(CalculatorContext* cc) override; + + ::mediapipe::Status Process(CalculatorContext* cc) override; + + private: + ::mediapipe::Status LoadLabelmap(std::string label_map_path); + + int top_k_ = -1; + float threshold_ = 0.0; + std::unordered_map label_map_; +}; +REGISTER_CALCULATOR(TopKScoresCalculator); + +::mediapipe::Status TopKScoresCalculator::GetContract(CalculatorContract* cc) { + RET_CHECK(cc->Inputs().HasTag("SCORES")); + cc->Inputs().Tag("SCORES").Set>(); + if (cc->Outputs().HasTag("TOP_K_INDEXES")) { + cc->Outputs().Tag("TOP_K_INDEXES").Set>(); + } + if (cc->Outputs().HasTag("TOP_K_SCORES")) { + cc->Outputs().Tag("TOP_K_SCORES").Set>(); + } + if (cc->Outputs().HasTag("TOP_K_LABELS")) { + cc->Outputs().Tag("TOP_K_LABELS").Set>(); + } + return ::mediapipe::OkStatus(); +} + +::mediapipe::Status TopKScoresCalculator::Open(CalculatorContext* cc) { + const auto& options = cc->Options<::mediapipe::TopKScoresCalculatorOptions>(); + RET_CHECK(options.has_top_k() || options.has_threshold()) + << "Must specify at least one of the top_k and threshold fields in " + "TopKScoresCalculatorOptions."; + if (options.has_top_k()) { + RET_CHECK(options.top_k() > 0) << "top_k must be greater than zero."; + top_k_ = options.top_k(); + } + if (options.has_threshold()) { + threshold_ = options.threshold(); + } + if (options.has_label_map_path()) { + MP_RETURN_IF_ERROR(LoadLabelmap(options.label_map_path())); + } + if (cc->Outputs().HasTag("TOP_K_LABELS")) { + RET_CHECK(!label_map_.empty()); + } + return ::mediapipe::OkStatus(); +} + +::mediapipe::Status TopKScoresCalculator::Process(CalculatorContext* cc) { + const std::vector& input_vector = + cc->Inputs().Tag("SCORES").Get>(); + std::vector top_k_indexes; + + std::vector top_k_scores; + + std::vector top_k_labels; + + if (top_k_ > 0) { + top_k_indexes.reserve(top_k_); + top_k_scores.reserve(top_k_); + top_k_labels.reserve(top_k_); + } + std::priority_queue, std::vector>, + std::greater>> + pq; + for (int i = 0; i < input_vector.size(); ++i) { + if (input_vector[i] < threshold_) { + continue; + } + if (top_k_ > 0) { + if (pq.size() < top_k_) { + pq.push(std::pair(input_vector[i], i)); + } else if (pq.top().first < input_vector[i]) { + pq.pop(); + pq.push(std::pair(input_vector[i], i)); + } + } else { + pq.push(std::pair(input_vector[i], i)); + } + } + + while (!pq.empty()) { + top_k_indexes.push_back(pq.top().second); + top_k_scores.push_back(pq.top().first); + pq.pop(); + } + 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")) { + for (int index : top_k_indexes) { + top_k_labels.push_back(label_map_[index]); + } + } + if (cc->Outputs().HasTag("TOP_K_INDEXES")) { + cc->Outputs() + .Tag("TOP_K_INDEXES") + .AddPacket(MakePacket>(top_k_indexes) + .At(cc->InputTimestamp())); + } + if (cc->Outputs().HasTag("TOP_K_SCORES")) { + cc->Outputs() + .Tag("TOP_K_SCORES") + .AddPacket(MakePacket>(top_k_scores) + .At(cc->InputTimestamp())); + } + if (cc->Outputs().HasTag("TOP_K_LABELS")) { + cc->Outputs() + .Tag("TOP_K_LABELS") + .AddPacket(MakePacket>(top_k_labels) + .At(cc->InputTimestamp())); + } + return ::mediapipe::OkStatus(); +} + +::mediapipe::Status TopKScoresCalculator::LoadLabelmap( + std::string label_map_path) { + std::string string_path; + ASSIGN_OR_RETURN(string_path, PathToResourceAsFile(label_map_path)); + std::string label_map_string; + MP_RETURN_IF_ERROR(file::GetContents(string_path, &label_map_string)); + + std::istringstream stream(label_map_string); + std::string line; + int i = 0; + while (std::getline(stream, line)) { + label_map_[i++] = line; + } + return ::mediapipe::OkStatus(); +} + +} // namespace mediapipe diff --git a/mediapipe/calculators/util/top_k_scores_calculator.proto b/mediapipe/calculators/util/top_k_scores_calculator.proto new file mode 100644 index 000000000..08fb7a756 --- /dev/null +++ b/mediapipe/calculators/util/top_k_scores_calculator.proto @@ -0,0 +1,33 @@ +// 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 TopKScoresCalculatorOptions { + extend CalculatorOptions { + optional TopKScoresCalculatorOptions ext = 271211788; + } + // How many highest scoring packets to output. + optional int32 top_k = 1; + + // If set, only keep the scores that are greater than the threshold. + optional float threshold = 2; + + // Path to a label map file for getting the actual name of classes. + optional string label_map_path = 3; +} diff --git a/mediapipe/calculators/util/top_k_scores_calculator_test.cc b/mediapipe/calculators/util/top_k_scores_calculator_test.cc new file mode 100644 index 000000000..7daeb5c0c --- /dev/null +++ b/mediapipe/calculators/util/top_k_scores_calculator_test.cc @@ -0,0 +1,150 @@ +// 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/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" + +namespace mediapipe { + +TEST(TopKScoresCalculatorTest, TestNodeConfig) { + CalculatorRunner runner(ParseTextProtoOrDie(R"( + calculator: "TopKScoresCalculator" + input_stream: "SCORES:score_vector" + output_stream: "TOP_K_INDEXES:top_k_indexes" + output_stream: "TOP_K_SCORES:top_k_scores" + options: { + [mediapipe.TopKScoresCalculatorOptions.ext] {} + } + )")); + + auto status = runner.Run(); + ASSERT_TRUE(!status.ok()); + EXPECT_THAT( + status.ToString(), + testing::HasSubstr( + "Must specify at least one of the top_k and threshold fields")); +} + +TEST(TopKScoresCalculatorTest, TestTopKOnly) { + CalculatorRunner runner(ParseTextProtoOrDie(R"( + calculator: "TopKScoresCalculator" + input_stream: "SCORES:score_vector" + output_stream: "TOP_K_INDEXES:top_k_indexes" + output_stream: "TOP_K_SCORES:top_k_scores" + options: { + [mediapipe.TopKScoresCalculatorOptions.ext] { top_k: 2 } + } + )")); + + std::vector score_vector{0.9, 0.2, 0.3, 1.0, 0.1}; + + runner.MutableInputs()->Tag("SCORES").packets.push_back( + MakePacket>(score_vector).At(Timestamp(0))); + + MP_ASSERT_OK(runner.Run()); + const std::vector& indexes_outputs = + runner.Outputs().Tag("TOP_K_INDEXES").packets; + ASSERT_EQ(1, indexes_outputs.size()); + const auto& indexes = indexes_outputs[0].Get>(); + EXPECT_EQ(2, indexes.size()); + EXPECT_EQ(3, indexes[0]); + EXPECT_EQ(0, indexes[1]); + const std::vector& scores_outputs = + runner.Outputs().Tag("TOP_K_SCORES").packets; + ASSERT_EQ(1, scores_outputs.size()); + const auto& scores = scores_outputs[0].Get>(); + EXPECT_EQ(2, scores.size()); + EXPECT_NEAR(1, scores[0], 1e-5); + EXPECT_NEAR(0.9, scores[1], 1e-5); +} + +TEST(TopKScoresCalculatorTest, TestThresholdOnly) { + CalculatorRunner runner(ParseTextProtoOrDie(R"( + calculator: "TopKScoresCalculator" + input_stream: "SCORES:score_vector" + output_stream: "TOP_K_INDEXES:top_k_indexes" + output_stream: "TOP_K_SCORES:top_k_scores" + options: { + [mediapipe.TopKScoresCalculatorOptions.ext] { threshold: 0.2 } + } + )")); + + std::vector score_vector{0.9, 0.2, 0.3, 1.0, 0.1}; + + runner.MutableInputs()->Tag("SCORES").packets.push_back( + MakePacket>(score_vector).At(Timestamp(0))); + + MP_ASSERT_OK(runner.Run()); + const std::vector& indexes_outputs = + runner.Outputs().Tag("TOP_K_INDEXES").packets; + ASSERT_EQ(1, indexes_outputs.size()); + const auto& indexes = indexes_outputs[0].Get>(); + EXPECT_EQ(4, indexes.size()); + EXPECT_EQ(3, indexes[0]); + EXPECT_EQ(0, indexes[1]); + EXPECT_EQ(2, indexes[2]); + EXPECT_EQ(1, indexes[3]); + const std::vector& scores_outputs = + runner.Outputs().Tag("TOP_K_SCORES").packets; + ASSERT_EQ(1, scores_outputs.size()); + const auto& scores = scores_outputs[0].Get>(); + EXPECT_EQ(4, scores.size()); + EXPECT_NEAR(1.0, scores[0], 1e-5); + EXPECT_NEAR(0.9, scores[1], 1e-5); + EXPECT_NEAR(0.3, scores[2], 1e-5); + EXPECT_NEAR(0.2, scores[3], 1e-5); +} + +TEST(TopKScoresCalculatorTest, TestBothTopKAndThreshold) { + CalculatorRunner runner(ParseTextProtoOrDie(R"( + calculator: "TopKScoresCalculator" + input_stream: "SCORES:score_vector" + output_stream: "TOP_K_INDEXES:top_k_indexes" + output_stream: "TOP_K_SCORES:top_k_scores" + options: { + [mediapipe.TopKScoresCalculatorOptions.ext] { top_k: 4 threshold: 0.3 } + } + )")); + + std::vector score_vector{0.9, 0.2, 0.3, 1.0, 0.1}; + + runner.MutableInputs()->Tag("SCORES").packets.push_back( + MakePacket>(score_vector).At(Timestamp(0))); + + MP_ASSERT_OK(runner.Run()); + const std::vector& indexes_outputs = + runner.Outputs().Tag("TOP_K_INDEXES").packets; + ASSERT_EQ(1, indexes_outputs.size()); + const auto& indexes = indexes_outputs[0].Get>(); + EXPECT_EQ(3, indexes.size()); + EXPECT_EQ(3, indexes[0]); + EXPECT_EQ(0, indexes[1]); + EXPECT_EQ(2, indexes[2]); + const std::vector& scores_outputs = + runner.Outputs().Tag("TOP_K_SCORES").packets; + ASSERT_EQ(1, scores_outputs.size()); + const auto& scores = scores_outputs[0].Get>(); + EXPECT_EQ(3, scores.size()); + EXPECT_NEAR(1.0, scores[0], 1e-5); + EXPECT_NEAR(0.9, scores[1], 1e-5); + EXPECT_NEAR(0.3, scores[2], 1e-5); +} + +} // namespace mediapipe diff --git a/mediapipe/docs/examples.md b/mediapipe/docs/examples.md index 6e3156283..404024b5f 100644 --- a/mediapipe/docs/examples.md +++ b/mediapipe/docs/examples.md @@ -51,6 +51,15 @@ and model details are described in the * [Android](./face_detection_mobile_gpu.md) * [iOS](./face_detection_mobile_gpu.md) +### Face Detection with CPU + +[Face Detection with CPU](./face_detection_mobile_cpu.md) illustrates using the +same TFLite model in a CPU-based pipeline. This example highlights how graphs +can be easily adapted to run on CPU v.s. GPU. + +* [Android](./face_detection_mobile_cpu.md) +* [iOS](./face_detection_mobile_cpu.md) + ### Hand Detection with GPU [Hand Detection with GPU](./hand_detection_mobile_gpu.md) illustrates how to use @@ -103,3 +112,29 @@ object detection models (TensorFlow and TFLite) using the MediaPipe C++ APIs. [Sobel edge detection]:https://en.wikipedia.org/wiki/Sobel_operator [CameraX]:https://developer.android.com/training/camerax + +### Face Detection on Desktop with Webcam + +[Face Detection on Desktop with Webcam](./face_detection_desktop.md) shows how +to use MediaPipe with a TFLite model for face detection on desktop using CPU or +GPU with live video from a webcam. + +* [Desktop GPU](./face_detection_desktop.md) +* [Desktop CPU](./face_detection_desktop.md) + +### Hand Tracking on Desktop with Webcam + +[Hand Tracking on Desktop with Webcam](./hand_tracking_desktop.md) shows how to +use MediaPipe with a TFLite model for hand tracking on desktop using CPU or GPU +with live video from a webcam. + +* [Desktop GPU](./hand_tracking_desktop.md) +* [Desktop CPU](./hand_tracking_desktop.md) + +### Hair Segmentation on Desktop with Webcam + +[Hair Segmentation on Desktop with Webcam](./hair_segmentation_desktop.md) shows +how to use MediaPipe with a TFLite model for hair segmentation on desktop using +GPU with live video from a webcam. + +* [Desktop GPU](./hair_segmentation_desktop.md) diff --git a/mediapipe/docs/face_detection_desktop.md b/mediapipe/docs/face_detection_desktop.md new file mode 100644 index 000000000..b95705262 --- /dev/null +++ b/mediapipe/docs/face_detection_desktop.md @@ -0,0 +1,265 @@ +## Face Detection on Desktop + +This is an example of using MediaPipe to run face detection models (TensorFlow +Lite) and render bounding boxes on the detected faces. To know more about the +face detection models, please refer to the model [`README file`]. Moreover, if +you are interested in running the same TensorfFlow Lite model on Android/iOS, +please see the +[Face Detection on GPU on Android/iOS](face_detection_mobile_gpu.md) and +[Face Detection on CPU on Android/iOS](face_detection_mobile_cpu.md) examples. + +We show the face detection demos with TensorFlow Lite model using the Webcam: + +- [TensorFlow Lite Face Detection Demo with Webcam (CPU)](#tensorflow-lite-face-detection-demo-with-webcam-cpu) + +- [TensorFlow Lite Face Detection Demo with Webcam (GPU)](#tensorflow-lite-face-detection-demo-with-webcam-gpu) + +Note: Desktop GPU works only on Linux. Mesa drivers need to be installed. Please +see +[step 4 of "Installing on Debian and Ubuntu" in the installation guide](./install.md). + +Note: If MediaPipe depends on OpenCV 2, please see the [known issues with OpenCV 2](#known-issues-with-opencv-2) section. + +### TensorFlow Lite Face Detection Demo with Webcam (CPU) + +To build and run the TensorFlow Lite example on desktop (CPU) with Webcam, run: + +```bash +# Video from webcam running on desktop CPU +$ bazel build -c opt --define MEDIAPIPE_DISABLE_GPU=1 \ + mediapipe/examples/desktop/face_detection:face_detection_cpu + +# It should print: +# Target //mediapipe/examples/desktop/face_detection:face_detection_cpu up-to-date: +# bazel-bin/mediapipe/examples/desktop/face_detection/face_detection_cpu +# INFO: Elapsed time: 36.417s, Critical Path: 23.22s +# 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 \ + --calculator_graph_config_file=mediapipe/graphs/face_detection/face_detection_desktop_live.pbtxt +``` + +### TensorFlow Lite Face Detection Demo with Webcam (GPU) + +To build and run the TensorFlow Lite example on desktop (GPU) with Webcam, run: + +```bash +# Video from webcam running on desktop GPU +# This works only for linux currently +$ bazel build -c opt --copt -DMESA_EGL_NO_X11_HEADERS \ + mediapipe/examples/desktop/face_detection:face_detection_gpu + +# It should print: +# Target //mediapipe/examples/desktop/face_detection:face_detection_gpu up-to-date: +# bazel-bin/mediapipe/examples/desktop/face_detection/face_detection_gpu +# INFO: Elapsed time: 36.417s, Critical Path: 23.22s +# 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 \ + --calculator_graph_config_file=mediapipe/graphs/face_detection/face_detection_mobile_gpu.pbtxt +``` + +#### Graph + +![graph visualization](images/face_detection_desktop.png) + +To visualize the graph as shown above, copy the text specification of the graph +below and paste it into +[MediaPipe Visualizer](https://viz.mediapipe.dev). + +```bash +# MediaPipe graph that performs face detection with TensorFlow Lite on CPU & GPU. +# Used in the examples in +# mediapipie/examples/desktop/face_detection:face_detection_cpu. + +# Images on CPU coming into and out of the graph. +input_stream: "input_video" +output_stream: "output_video" + +# Throttles the images flowing downstream for flow control. It passes through +# the very first incoming image unaltered, and waits for +# TfLiteTensorsToDetectionsCalculator downstream in the graph to finish +# generating the corresponding detections before it passes through another +# image. All images that come in while waiting are dropped, limiting the number +# of in-flight images between this calculator and +# TfLiteTensorsToDetectionsCalculator to 1. This prevents the nodes in between +# from queuing up incoming images and data excessively, which leads to increased +# latency and memory usage, unwanted in real-time mobile applications. It also +# eliminates unnecessarily computation, e.g., a transformed image produced by +# ImageTransformationCalculator may get dropped downstream if the subsequent +# TfLiteConverterCalculator or TfLiteInferenceCalculator is still busy +# processing previous inputs. +node { + calculator: "FlowLimiterCalculator" + input_stream: "input_video" + input_stream: "FINISHED:detections" + input_stream_info: { + tag_index: "FINISHED" + back_edge: true + } + output_stream: "throttled_input_video" +} + +# Transforms the input image on CPU to a 128x128 image. To scale the input +# image, the scale_mode option is set to FIT to preserve the aspect ratio, +# resulting in potential letterboxing in the transformed image. +node: { + calculator: "ImageTransformationCalculator" + input_stream: "IMAGE:throttled_input_video" + output_stream: "IMAGE:transformed_input_video_cpu" + output_stream: "LETTERBOX_PADDING:letterbox_padding" + node_options: { + [type.googleapis.com/mediapipe.ImageTransformationCalculatorOptions] { + output_width: 128 + output_height: 128 + scale_mode: FIT + } + } +} + +# Converts the transformed input image on CPU into an image tensor stored as a +# TfLiteTensor. +node { + calculator: "TfLiteConverterCalculator" + input_stream: "IMAGE:transformed_input_video_cpu" + output_stream: "TENSORS:image_tensor" +} + +# Runs a TensorFlow Lite model on CPU that takes an image tensor and outputs a +# vector of tensors representing, for instance, detection boxes/keypoints and +# scores. +node { + calculator: "TfLiteInferenceCalculator" + input_stream: "TENSORS:image_tensor" + output_stream: "TENSORS:detection_tensors" + node_options: { + [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] { + model_path: "mediapipe/models/face_detection_front.tflite" + } + } +} + +# Generates a single side packet containing a vector of SSD anchors based on +# the specification in the options. +node { + calculator: "SsdAnchorsCalculator" + output_side_packet: "anchors" + node_options: { + [type.googleapis.com/mediapipe.SsdAnchorsCalculatorOptions] { + num_layers: 4 + min_scale: 0.1484375 + max_scale: 0.75 + input_size_height: 128 + input_size_width: 128 + anchor_offset_x: 0.5 + anchor_offset_y: 0.5 + strides: 8 + strides: 16 + strides: 16 + strides: 16 + aspect_ratios: 1.0 + fixed_anchor_size: true + } + } +} + +# Decodes the detection tensors generated by the TensorFlow Lite model, based on +# the SSD anchors and the specification in the options, into a vector of +# detections. Each detection describes a detected object. +node { + calculator: "TfLiteTensorsToDetectionsCalculator" + input_stream: "TENSORS:detection_tensors" + input_side_packet: "ANCHORS:anchors" + output_stream: "DETECTIONS:detections" + node_options: { + [type.googleapis.com/mediapipe.TfLiteTensorsToDetectionsCalculatorOptions] { + num_classes: 1 + num_boxes: 896 + num_coords: 16 + box_coord_offset: 0 + keypoint_coord_offset: 4 + num_keypoints: 6 + num_values_per_keypoint: 2 + sigmoid_score: true + score_clipping_thresh: 100.0 + reverse_output_order: true + x_scale: 128.0 + y_scale: 128.0 + h_scale: 128.0 + w_scale: 128.0 + min_score_thresh: 0.75 + } + } +} + +# Performs non-max suppression to remove excessive detections. +node { + calculator: "NonMaxSuppressionCalculator" + input_stream: "detections" + output_stream: "filtered_detections" + node_options: { + [type.googleapis.com/mediapipe.NonMaxSuppressionCalculatorOptions] { + min_suppression_threshold: 0.3 + overlap_type: INTERSECTION_OVER_UNION + algorithm: WEIGHTED + return_empty_detections: true + } + } +} + +# Maps detection label IDs to the corresponding label text ("Face"). The label +# map is provided in the label_map_path option. +node { + calculator: "DetectionLabelIdToTextCalculator" + input_stream: "filtered_detections" + output_stream: "labeled_detections" + node_options: { + [type.googleapis.com/mediapipe.DetectionLabelIdToTextCalculatorOptions] { + label_map_path: "mediapipe/models/face_detection_front_labelmap.txt" + } + } +} + +# Adjusts detection locations (already normalized to [0.f, 1.f]) on the +# letterboxed image (after image transformation with the FIT scale mode) to the +# corresponding locations on the same image with the letterbox removed (the +# input image to the graph before image transformation). +node { + calculator: "DetectionLetterboxRemovalCalculator" + input_stream: "DETECTIONS:labeled_detections" + input_stream: "LETTERBOX_PADDING:letterbox_padding" + output_stream: "DETECTIONS:output_detections" +} + +# Converts the detections to drawing primitives for annotation overlay. +node { + calculator: "DetectionsToRenderDataCalculator" + input_stream: "DETECTIONS:output_detections" + output_stream: "RENDER_DATA:render_data" + node_options: { + [type.googleapis.com/mediapipe.DetectionsToRenderDataCalculatorOptions] { + thickness: 4.0 + color { r: 255 g: 0 b: 0 } + } + } +} + +# Draws annotations and overlays them on top of the input images. +node { + calculator: "AnnotationOverlayCalculator" + input_stream: "INPUT_FRAME:throttled_input_video" + input_stream: "render_data" + output_stream: "OUTPUT_FRAME:output_video" +} +``` + +[`README file`]:https://github.com/google/mediapipe/tree/master/mediapipe/models/object_detection_saved_model/README.md diff --git a/mediapipe/docs/face_detection_mobile_cpu.md b/mediapipe/docs/face_detection_mobile_cpu.md new file mode 100644 index 000000000..bcef66a48 --- /dev/null +++ b/mediapipe/docs/face_detection_mobile_cpu.md @@ -0,0 +1,244 @@ +# Face Detection (CPU) + +This doc focuses on the +[example graph](https://github.com/google/mediapipe/tree/master/mediapipe/graphs/face_detection/face_detection_mobile_cpu.pbtxt) +that performs face detection with TensorFlow Lite on CPU. + +## Android + +[Source](https://github.com/google/mediapipe/tree/master/mediapipe/examples/android/src/java/com/google/mediapipe/apps/facedetectioncpu) + +To build and install the app: + +```bash +bazel build -c opt --config=android_arm64 mediapipe/examples/android/src/java/com/google/mediapipe/apps/facedetectioncpu +adb install bazel-bin/mediapipe/examples/android/src/java/com/google/mediapipe/apps/facedetectioncpu/facedetectioncpu.apk +``` + +## iOS + +[Source](https://github.com/google/mediapipe/tree/master/mediapipe/examples/ios/facedetectioncpu). + +See the general [instructions](./mediapipe_ios_setup.md) for building iOS +examples and generating an Xcode project. This will be the FaceDetectionCpuApp +target. + +To build on the command line: + +```bash +bazel build -c opt --config=ios_arm64 mediapipe/examples/ios/facedetectioncpu:FaceDetectionCpuApp +``` + +## Graph + +![face_detection_mobile_cpu_graph](images/mobile/face_detection_mobile_cpu.png) + +To visualize the graph as shown above, copy the text specification of the graph +below and paste it into [MediaPipe Visualizer](https://viz.mediapipe.dev/). + +[Source pbtxt file](https://github.com/google/mediapipe/tree/master/mediapipe/graphs/face_detection/face_detection_mobile_cpu.pbtxt) + +```bash +# MediaPipe graph that performs face detection with TensorFlow Lite on CPU. +# Used in the examples in +# mediapipie/examples/android/src/java/com/mediapipe/apps/facedetectioncpu and +# mediapipie/examples/ios/facedetectioncpu. + +# Images on GPU coming into and out of the graph. +input_stream: "input_video" +output_stream: "output_video" + +# Throttles the images flowing downstream for flow control. It passes through +# the very first incoming image unaltered, and waits for +# TfLiteTensorsToDetectionsCalculator downstream in the graph to finish +# generating the corresponding detections before it passes through another +# image. All images that come in while waiting are dropped, limiting the number +# of in-flight images between this calculator and +# TfLiteTensorsToDetectionsCalculator to 1. This prevents the nodes in between +# from queuing up incoming images and data excessively, which leads to increased +# latency and memory usage, unwanted in real-time mobile applications. It also +# eliminates unnecessarily computation, e.g., a transformed image produced by +# ImageTransformationCalculator may get dropped downstream if the subsequent +# TfLiteConverterCalculator or TfLiteInferenceCalculator is still busy +# processing previous inputs. +node { + calculator: "FlowLimiterCalculator" + input_stream: "input_video" + input_stream: "FINISHED:detections" + input_stream_info: { + tag_index: "FINISHED" + back_edge: true + } + output_stream: "throttled_input_video" +} + +# Transfers the input image from GPU to CPU memory for the purpose of +# demonstrating a CPU-based pipeline. Note that the input image on GPU has the +# origin defined at the bottom-left corner (OpenGL convention). As a result, +# the transferred image on CPU also shares the same representation. +node: { + calculator: "GpuBufferToImageFrameCalculator" + input_stream: "throttled_input_video" + output_stream: "input_video_cpu" +} + +# Transforms the input image on CPU to a 128x128 image. To scale the input +# image, the scale_mode option is set to FIT to preserve the aspect ratio, +# resulting in potential letterboxing in the transformed image. +node: { + calculator: "ImageTransformationCalculator" + input_stream: "IMAGE:input_video_cpu" + output_stream: "IMAGE:transformed_input_video_cpu" + output_stream: "LETTERBOX_PADDING:letterbox_padding" + node_options: { + [type.googleapis.com/mediapipe.ImageTransformationCalculatorOptions] { + output_width: 128 + output_height: 128 + scale_mode: FIT + } + } +} + +# Converts the transformed input image on CPU into an image tensor stored as a +# TfLiteTensor. +node { + calculator: "TfLiteConverterCalculator" + input_stream: "IMAGE:transformed_input_video_cpu" + output_stream: "TENSORS:image_tensor" +} + +# Runs a TensorFlow Lite model on CPU that takes an image tensor and outputs a +# vector of tensors representing, for instance, detection boxes/keypoints and +# scores. +node { + calculator: "TfLiteInferenceCalculator" + input_stream: "TENSORS:image_tensor" + output_stream: "TENSORS:detection_tensors" + node_options: { + [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] { + model_path: "mediapipe/models/face_detection_front.tflite" + } + } +} + +# Generates a single side packet containing a vector of SSD anchors based on +# the specification in the options. +node { + calculator: "SsdAnchorsCalculator" + output_side_packet: "anchors" + node_options: { + [type.googleapis.com/mediapipe.SsdAnchorsCalculatorOptions] { + num_layers: 4 + min_scale: 0.1484375 + max_scale: 0.75 + input_size_height: 128 + input_size_width: 128 + anchor_offset_x: 0.5 + anchor_offset_y: 0.5 + strides: 8 + strides: 16 + strides: 16 + strides: 16 + aspect_ratios: 1.0 + fixed_anchor_size: true + } + } +} + +# Decodes the detection tensors generated by the TensorFlow Lite model, based on +# the SSD anchors and the specification in the options, into a vector of +# detections. Each detection describes a detected object. +node { + calculator: "TfLiteTensorsToDetectionsCalculator" + input_stream: "TENSORS:detection_tensors" + input_side_packet: "ANCHORS:anchors" + output_stream: "DETECTIONS:detections" + node_options: { + [type.googleapis.com/mediapipe.TfLiteTensorsToDetectionsCalculatorOptions] { + num_classes: 1 + num_boxes: 896 + num_coords: 16 + box_coord_offset: 0 + keypoint_coord_offset: 4 + num_keypoints: 6 + num_values_per_keypoint: 2 + sigmoid_score: true + score_clipping_thresh: 100.0 + reverse_output_order: true + x_scale: 128.0 + y_scale: 128.0 + h_scale: 128.0 + w_scale: 128.0 + min_score_thresh: 0.75 + } + } +} + +# Performs non-max suppression to remove excessive detections. +node { + calculator: "NonMaxSuppressionCalculator" + input_stream: "detections" + output_stream: "filtered_detections" + node_options: { + [type.googleapis.com/mediapipe.NonMaxSuppressionCalculatorOptions] { + min_suppression_threshold: 0.3 + overlap_type: INTERSECTION_OVER_UNION + algorithm: WEIGHTED + return_empty_detections: true + } + } +} + +# Maps detection label IDs to the corresponding label text ("Face"). The label +# map is provided in the label_map_path option. +node { + calculator: "DetectionLabelIdToTextCalculator" + input_stream: "filtered_detections" + output_stream: "labeled_detections" + node_options: { + [type.googleapis.com/mediapipe.DetectionLabelIdToTextCalculatorOptions] { + label_map_path: "mediapipe/models/face_detection_front_labelmap.txt" + } + } +} + +# Adjusts detection locations (already normalized to [0.f, 1.f]) on the +# letterboxed image (after image transformation with the FIT scale mode) to the +# corresponding locations on the same image with the letterbox removed (the +# input image to the graph before image transformation). +node { + calculator: "DetectionLetterboxRemovalCalculator" + input_stream: "DETECTIONS:labeled_detections" + input_stream: "LETTERBOX_PADDING:letterbox_padding" + output_stream: "DETECTIONS:output_detections" +} + +# Converts the detections to drawing primitives for annotation overlay. +node { + calculator: "DetectionsToRenderDataCalculator" + input_stream: "DETECTIONS:output_detections" + output_stream: "RENDER_DATA:render_data" + node_options: { + [type.googleapis.com/mediapipe.DetectionsToRenderDataCalculatorOptions] { + thickness: 4.0 + color { r: 255 g: 0 b: 0 } + } + } +} + +# Draws annotations and overlays them on top of the input images. +node { + calculator: "AnnotationOverlayCalculator" + input_stream: "INPUT_FRAME:input_video_cpu" + input_stream: "render_data" + output_stream: "OUTPUT_FRAME:output_video_cpu" +} + +# Transfers the annotated image from CPU back to GPU memory, to be sent out of +# the graph. +node: { + calculator: "ImageFrameToGpuBufferCalculator" + input_stream: "output_video_cpu" + output_stream: "output_video" +} +``` diff --git a/mediapipe/docs/hair_segmentation_desktop.md b/mediapipe/docs/hair_segmentation_desktop.md new file mode 100644 index 000000000..058902363 --- /dev/null +++ b/mediapipe/docs/hair_segmentation_desktop.md @@ -0,0 +1,209 @@ +## Hair Segmentation on Desktop + +This is an example of using MediaPipe to run hair segmentation models +(TensorFlow Lite) and render a color to the detected hair. To know more about +the hair segmentation models, please refer to the model [`README file`]. +Moreover, if you are interested in running the same TensorfFlow Lite model on +Android/iOS, please see the +[Hair Segmentation on GPU on Android/iOS](hair_segmentation_mobile_gpu.md) and + +We show the hair segmentation demos with TensorFlow Lite model using the Webcam: + +- [TensorFlow Lite Hair Segmentation Demo with Webcam (GPU)](#tensorflow-lite-hair-segmentation-demo-with-webcam-gpu) + +Note: Desktop GPU works only on Linux. Mesa drivers need to be installed. Please +see +[step 4 of "Installing on Debian and Ubuntu" in the installation guide](./install.md). + +Note: If MediaPipe depends on OpenCV 2, please see the [known issues with OpenCV 2](#known-issues-with-opencv-2) section. + +### TensorFlow Lite Hair Segmentation Demo with Webcam (GPU) + +To build and run the TensorFlow Lite example on desktop (GPU) with Webcam, run: + +```bash +# Video from webcam running on desktop GPU +# This works only for linux currently +$ bazel build -c opt --copt -DMESA_EGL_NO_X11_HEADERS \ + mediapipe/examples/desktop/hair_segmentation:hair_segmentation_gpu + +# It should print: +#INFO: Found 1 target... +#Target //mediapipe/examples/desktop/hair_segmentation:hair_segmentation_gpu up-to-date: +# bazel-bin/mediapipe/examples/desktop/hair_segmentation/hair_segmentation_gpu +#INFO: Elapsed time: 18.209s, Forge stats: 13026/13057 actions cached, 20.8s CPU used, 0.0s queue time, 89.3 MB ObjFS output (novel bytes: 87.4 MB), 0.0 MB local output, Critical Path: 11.88s, Remote (86.01% of the time): [queue: 0.00%, network: 16.83%, setup: 4.59%, process: 38.92%] +#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 \ + --calculator_graph_config_file=mediapipe/graphs/hair_segmentation/hair_segmentation_mobile_gpu.pbtxt +``` + +#### Graph + +![hair_segmentation_mobile_gpu_graph](images/mobile/hair_segmentation_mobile_gpu.png) + +To visualize the graph as shown above, copy the text specification of the graph +below and paste it into +[MediaPipe Visualizer](https://viz.mediapipe.dev). + +```bash +# MediaPipe graph that performs hair segmentation with TensorFlow Lite on GPU. +# Used in the example in +# mediapipie/examples/android/src/java/com/mediapipe/apps/hairsegmentationgpu. + +# Images on GPU coming into and out of the graph. +input_stream: "input_video" +output_stream: "output_video" + +# Throttles the images flowing downstream for flow control. It passes through +# the very first incoming image unaltered, and waits for +# TfLiteTensorsToSegmentationCalculator downstream in the graph to finish +# generating the corresponding hair mask before it passes through another +# image. All images that come in while waiting are dropped, limiting the number +# of in-flight images between this calculator and +# TfLiteTensorsToSegmentationCalculator to 1. This prevents the nodes in between +# from queuing up incoming images and data excessively, which leads to increased +# latency and memory usage, unwanted in real-time mobile applications. It also +# eliminates unnecessarily computation, e.g., a transformed image produced by +# ImageTransformationCalculator may get dropped downstream if the subsequent +# TfLiteConverterCalculator or TfLiteInferenceCalculator is still busy +# processing previous inputs. +node { + calculator: "FlowLimiterCalculator" + input_stream: "input_video" + input_stream: "FINISHED:hair_mask" + input_stream_info: { + tag_index: "FINISHED" + back_edge: true + } + output_stream: "throttled_input_video" +} + +# Transforms the input image on GPU to a 512x512 image. To scale the image, by +# default it uses the STRETCH scale mode that maps the entire input image to the +# entire transformed image. As a result, image aspect ratio may be changed and +# objects in the image may be deformed (stretched or squeezed), but the hair +# segmentation model used in this graph is agnostic to that deformation. +node: { + calculator: "ImageTransformationCalculator" + input_stream: "IMAGE_GPU:throttled_input_video" + output_stream: "IMAGE_GPU:transformed_input_video" + node_options: { + [type.googleapis.com/mediapipe.ImageTransformationCalculatorOptions] { + output_width: 512 + output_height: 512 + } + } +} + +# Caches a mask fed back from the previous round of hair segmentation, and upon +# the arrival of the next input image sends out the cached mask with the +# timestamp replaced by that of the input image, essentially generating a packet +# that carries the previous mask. Note that upon the arrival of the very first +# input image, an empty packet is sent out to jump start the feedback loop. +node { + calculator: "PreviousLoopbackCalculator" + input_stream: "MAIN:throttled_input_video" + input_stream: "LOOP:hair_mask" + input_stream_info: { + tag_index: "LOOP" + back_edge: true + } + output_stream: "PREV_LOOP:previous_hair_mask" +} + +# Embeds the hair mask generated from the previous round of hair segmentation +# as the alpha channel of the current input image. +node { + calculator: "SetAlphaCalculator" + input_stream: "IMAGE_GPU:transformed_input_video" + input_stream: "ALPHA_GPU:previous_hair_mask" + output_stream: "IMAGE_GPU:mask_embedded_input_video" +} + +# Converts the transformed input image on GPU into an image tensor stored in +# tflite::gpu::GlBuffer. The zero_center option is set to false to normalize the +# pixel values to [0.f, 1.f] as opposed to [-1.f, 1.f]. With the +# max_num_channels option set to 4, all 4 RGBA channels are contained in the +# image tensor. +node { + calculator: "TfLiteConverterCalculator" + input_stream: "IMAGE_GPU:mask_embedded_input_video" + output_stream: "TENSORS_GPU:image_tensor" + node_options: { + [type.googleapis.com/mediapipe.TfLiteConverterCalculatorOptions] { + zero_center: false + max_num_channels: 4 + } + } +} + +# Generates a single side packet containing a TensorFlow Lite op resolver that +# supports custom ops needed by the model used in this graph. +node { + calculator: "TfLiteCustomOpResolverCalculator" + output_side_packet: "op_resolver" + node_options: { + [type.googleapis.com/mediapipe.TfLiteCustomOpResolverCalculatorOptions] { + use_gpu: true + } + } +} + +# Runs a TensorFlow Lite model on GPU that takes an image tensor and outputs a +# tensor representing the hair segmentation, which has the same width and height +# as the input image tensor. +node { + calculator: "TfLiteInferenceCalculator" + input_stream: "TENSORS_GPU:image_tensor" + output_stream: "TENSORS_GPU:segmentation_tensor" + input_side_packet: "CUSTOM_OP_RESOLVER:op_resolver" + node_options: { + [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] { + model_path: "mediapipe/models/hair_segmentation.tflite" + use_gpu: true + } + } +} + +# Decodes the segmentation tensor generated by the TensorFlow Lite model into a +# mask of values in [0.f, 1.f], stored in the R channel of a GPU buffer. It also +# takes the mask generated previously as another input to improve the temporal +# consistency. +node { + calculator: "TfLiteTensorsToSegmentationCalculator" + input_stream: "TENSORS_GPU:segmentation_tensor" + input_stream: "PREV_MASK_GPU:previous_hair_mask" + output_stream: "MASK_GPU:hair_mask" + node_options: { + [type.googleapis.com/mediapipe.TfLiteTensorsToSegmentationCalculatorOptions] { + tensor_width: 512 + tensor_height: 512 + tensor_channels: 2 + combine_with_previous_ratio: 0.9 + output_layer_index: 1 + } + } +} + +# Colors the hair segmentation with the color specified in the option. +node { + calculator: "RecolorCalculator" + input_stream: "IMAGE_GPU:throttled_input_video" + input_stream: "MASK_GPU:hair_mask" + output_stream: "IMAGE_GPU:output_video" + node_options: { + [type.googleapis.com/mediapipe.RecolorCalculatorOptions] { + color { r: 0 g: 0 b: 255 } + mask_channel: RED + } + } +} +``` + +[`README file`]:https://github.com/google/mediapipe/tree/master/mediapipe/README.md diff --git a/mediapipe/docs/hand_detection_mobile_gpu.md b/mediapipe/docs/hand_detection_mobile_gpu.md index 141c0a26f..5c4a41bbd 100644 --- a/mediapipe/docs/hand_detection_mobile_gpu.md +++ b/mediapipe/docs/hand_detection_mobile_gpu.md @@ -1,7 +1,7 @@ # Hand Detection (GPU) This doc focuses on the -[example graph](https://github.com/google/mediapipe/tree/master/mediapipe/graphs/hand_tracking/hand_detection_gpu.pbtxt) +[example graph](https://github.com/google/mediapipe/tree/master/mediapipe/graphs/hand_tracking/hand_detection_mobile.pbtxt) that performs hand detection with TensorFlow Lite on GPU. It is related to the [hand tracking example](./hand_tracking_mobile_gpu.md). @@ -147,7 +147,7 @@ node { ![hand_detection_gpu_subgraph](images/mobile/hand_detection_gpu_subgraph.png) -[Source pbtxt file](https://github.com/google/mediapipe/tree/master/mediapipe/graphs/hand_tracking/hand_detection_gpu.pbtxt) +[Source pbtxt file](https://github.com/google/mediapipe/tree/master/mediapipe/graphs/hand_tracking/subgraphs/hand_detection_gpu.pbtxt) ```bash # MediaPipe hand detection subgraph. diff --git a/mediapipe/docs/hand_tracking_desktop.md b/mediapipe/docs/hand_tracking_desktop.md new file mode 100644 index 000000000..6776a4710 --- /dev/null +++ b/mediapipe/docs/hand_tracking_desktop.md @@ -0,0 +1,184 @@ +## Hand Tracking on Desktop + +This is an example of using MediaPipe to run hand tracking models (TensorFlow +Lite) and render bounding boxes on the detected hand (one hand only). To know +more about the hand tracking models, please refer to the model [`README file`]. +Moreover, if you are interested in running the same TensorfFlow Lite model on +Android/iOS, please see the +[Hand Tracking on GPU on Android/iOS](hand_tracking_mobile_gpu.md) and + +We show the hand tracking demos with TensorFlow Lite model using the Webcam: + +- [TensorFlow Lite Hand Tracking Demo with Webcam (CPU)](#tensorflow-lite-hand-tracking-demo-with-webcam-cpu) + +- [TensorFlow Lite Hand Tracking Demo with Webcam (GPU)](#tensorflow-lite-hand-tracking-demo-with-webcam-gpu) + +Note: Desktop GPU works only on Linux. Mesa drivers need to be installed. Please +see +[step 4 of "Installing on Debian and Ubuntu" in the installation guide](./install.md). + +Note: If MediaPipe depends on OpenCV 2, please see the [known issues with OpenCV 2](#known-issues-with-opencv-2) section. + +### TensorFlow Lite Hand Tracking Demo with Webcam (CPU) + +To build and run the TensorFlow Lite example on desktop (CPU) with Webcam, run: + +```bash +# Video from webcam running on desktop CPU +$ bazel build -c opt --define MEDIAPIPE_DISABLE_GPU=1 \ + mediapipe/examples/desktop/hand_tracking:hand_tracking_cpu + +# It should print: +#Target //mediapipe/examples/desktop/hand_tracking:hand_tracking_cpu up-to-date: +# bazel-bin/mediapipe/examples/desktop/hand_tracking/hand_tracking_cpu +#INFO: Elapsed time: 22.645s, Forge stats: 13356/13463 actions cached, 1.5m CPU used, 0.0s queue time, 819.8 MB ObjFS output (novel bytes: 85.6 MB), 0.0 MB local output, Critical Path: 14.43s, Remote (87.25% of the time): [queue: 0.00%, network: 14.88%, setup: 4.80%, process: 39.80%, fetch: 18.15%] +#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 \ + --calculator_graph_config_file=mediapipe/graphs/hand_tracking/hand_tracking_desktop_live.pbtxt +``` + +### TensorFlow Lite Hand Tracking Demo with Webcam (GPU) + +To build and run the TensorFlow Lite example on desktop (GPU) with Webcam, run: + +```bash +# Video from webcam running on desktop GPU +# This works only for linux currently +$ bazel build -c opt --copt -DMESA_EGL_NO_X11_HEADERS \ + mediapipe/examples/desktop/hand_tracking:hand_tracking_gpu + +# It should print: +# Target //mediapipe/examples/desktop/hand_tracking:hand_tracking_gpu up-to-date: +# bazel-bin/mediapipe/examples/desktop/hand_tracking/hand_tracking_gpu +#INFO: Elapsed time: 84.055s, Forge stats: 6858/19343 actions cached, 1.6h CPU used, 0.9s queue time, 1.68 GB ObjFS output (novel bytes: 485.1 MB), 0.0 MB local output, Critical Path: 48.14s, Remote (99.40% of the time): [queue: 0.00%, setup: 5.59%, process: 74.44%] +#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 \ + --calculator_graph_config_file=mediapipe/graphs/hand_tracking/hand_tracking_mobile.pbtxt +``` + +#### Graph + +![graph visualization](images/hand_tracking_desktop.png) + +To visualize the graph as shown above, copy the text specification of the graph +below and paste it into +[MediaPipe Visualizer](https://viz.mediapipe.dev). + +```bash +# MediaPipe graph that performs hand tracking on desktop with TensorFlow Lite +# on CPU & GPU. +# Used in the example in +# mediapipie/examples/desktop/hand_tracking:hand_tracking_cpu. + +# Images coming into and out of the graph. +input_stream: "input_video" +output_stream: "output_video" + +# Caches a hand-presence decision fed back from HandLandmarkSubgraph, and upon +# the arrival of the next input image sends out the cached decision with the +# timestamp replaced by that of the input image, essentially generating a packet +# that carries the previous hand-presence decision. Note that upon the arrival +# of the very first input image, an empty packet is sent out to jump start the +# feedback loop. +node { + calculator: "PreviousLoopbackCalculator" + input_stream: "MAIN:input_video" + input_stream: "LOOP:hand_presence" + input_stream_info: { + tag_index: "LOOP" + back_edge: true + } + output_stream: "PREV_LOOP:prev_hand_presence" +} + +# Drops the incoming image if HandLandmarkSubgraph was able to identify hand +# presence in the previous image. Otherwise, passes the incoming image through +# to trigger a new round of hand detection in HandDetectionSubgraph. +node { + calculator: "GateCalculator" + input_stream: "input_video" + input_stream: "DISALLOW:prev_hand_presence" + output_stream: "hand_detection_input_video" + + node_options: { + [type.googleapis.com/mediapipe.GateCalculatorOptions] { + empty_packets_as_allow: true + } + } +} + +# Subgraph that detections hands (see hand_detection_cpu.pbtxt). +node { + calculator: "HandDetectionSubgraph" + input_stream: "hand_detection_input_video" + output_stream: "DETECTIONS:palm_detections" + output_stream: "NORM_RECT:hand_rect_from_palm_detections" +} + +# Subgraph that localizes hand landmarks (see hand_landmark_cpu.pbtxt). +node { + calculator: "HandLandmarkSubgraph" + input_stream: "IMAGE:input_video" + input_stream: "NORM_RECT:hand_rect" + output_stream: "LANDMARKS:hand_landmarks" + output_stream: "NORM_RECT:hand_rect_from_landmarks" + output_stream: "PRESENCE:hand_presence" +} + +# Caches a hand rectangle fed back from HandLandmarkSubgraph, and upon the +# arrival of the next input image sends out the cached rectangle with the +# timestamp replaced by that of the input image, essentially generating a packet +# that carries the previous hand rectangle. Note that upon the arrival of the +# very first input image, an empty packet is sent out to jump start the +# feedback loop. +node { + calculator: "PreviousLoopbackCalculator" + input_stream: "MAIN:input_video" + input_stream: "LOOP:hand_rect_from_landmarks" + input_stream_info: { + tag_index: "LOOP" + back_edge: true + } + output_stream: "PREV_LOOP:prev_hand_rect_from_landmarks" +} + +# Merges a stream of hand rectangles generated by HandDetectionSubgraph and that +# generated by HandLandmarkSubgraph into a single output stream by selecting +# between one of the two streams. The former is selected if the incoming packet +# is not empty, i.e., hand detection is performed on the current image by +# HandDetectionSubgraph (because HandLandmarkSubgraph could not identify hand +# presence in the previous image). Otherwise, the latter is selected, which is +# never empty because HandLandmarkSubgraphs processes all images (that went +# through FlowLimiterCaculator). +node { + calculator: "MergeCalculator" + input_stream: "hand_rect_from_palm_detections" + input_stream: "prev_hand_rect_from_landmarks" + output_stream: "hand_rect" +} + +# Subgraph that renders annotations and overlays them on top of the input +# images (see renderer_cpu.pbtxt). +node { + calculator: "RendererSubgraph" + input_stream: "IMAGE:input_video" + input_stream: "LANDMARKS:hand_landmarks" + input_stream: "NORM_RECT:hand_rect" + input_stream: "DETECTIONS:palm_detections" + output_stream: "IMAGE:output_video" +} + +``` + +[`README file`]:https://github.com/google/mediapipe/tree/master/mediapipe/README.md diff --git a/mediapipe/docs/hand_tracking_mobile_gpu.md b/mediapipe/docs/hand_tracking_mobile_gpu.md index 08af5dcc5..be9cdd264 100644 --- a/mediapipe/docs/hand_tracking_mobile_gpu.md +++ b/mediapipe/docs/hand_tracking_mobile_gpu.md @@ -227,7 +227,7 @@ node { ![hand_detection_gpu_subgraph](images/mobile/hand_detection_gpu_subgraph.png) -[Source pbtxt file](https://github.com/google/mediapipe/tree/master/mediapipe/graphs/hand_tracking/hand_detection_gpu.pbtxt) +[Source pbtxt file](https://github.com/google/mediapipe/tree/master/mediapipe/graphs/hand_tracking/subgraphs/hand_detection_gpu.pbtxt) ```bash # MediaPipe hand detection subgraph. @@ -433,7 +433,7 @@ node { ![hand_landmark_gpu_subgraph.pbtxt](images/mobile/hand_landmark_gpu_subgraph.png) -[Source pbtxt file](https://github.com/google/mediapipe/tree/master/mediapipe/graphs/hand_tracking/hand_landmark_gpu.pbtxt) +[Source pbtxt file](https://github.com/google/mediapipe/tree/master/mediapipe/graphs/hand_tracking/subgraphs/hand_landmark_gpu.pbtxt) ```bash # MediaPipe hand landmark localization subgraph. @@ -617,7 +617,7 @@ node { ![hand_renderer_gpu_subgraph.pbtxt](images/mobile/hand_renderer_gpu_subgraph.png) -[Source pbtxt file](https://github.com/google/mediapipe/tree/master/mediapipe/graphs/hand_tracking/renderer_gpu.pbtxt) +[Source pbtxt file](https://github.com/google/mediapipe/tree/master/mediapipe/graphs/hand_tracking/subgraphs/renderer_gpu.pbtxt) ```bash # MediaPipe hand tracking rendering subgraph. diff --git a/mediapipe/docs/images/face_detection_desktop.png b/mediapipe/docs/images/face_detection_desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..7f5f8ab1bc8eae8afb399aad3858afa0934bc2ed GIT binary patch literal 121035 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 A8~^|S literal 0 HcmV?d00001 diff --git a/mediapipe/docs/images/hand_tracking_desktop.png b/mediapipe/docs/images/hand_tracking_desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..cfc38f057d3949a66832d58521d10478088a6072 GIT binary patch literal 98586 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n;m?VphZkfbq>zc!%ZtU;(BfDeHijrlqFTWNzN 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 z℘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 literal 0 HcmV?d00001 diff --git a/mediapipe/docs/images/mobile/hand_tracking_mobile.png b/mediapipe/docs/images/mobile/hand_tracking_mobile.png index 66b9a7a9e60138e43afdc251e50cf72a712dd93b..3b2063190b03ad00acd22422ba298b7f4cb83d81 100644 GIT binary patch literal 53355 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�(uR(u=ul@ml`^^ZJ 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 Uvv2;rGz$1plzk~vEd>kyU&B#^%>V!Z literal 49994 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 s0;r1E,output_video_path= ``` +### TensorFlow Lite Object Detection Demo with Webcam (CPU) + +To build and run the TensorFlow Lite example on desktop (CPU) with Webcam, run: + +```bash +# Video from webcam running on desktop CPU +$ bazel build -c opt --define MEDIAPIPE_DISABLE_GPU=1 \ + mediapipe/examples/desktop/object_detection:object_detection_cpu + +# It should print: +#Target //mediapipe/examples/desktop/object_detection:object_detection_cpu up-to-date: +# bazel-bin/mediapipe/examples/desktop/object_detection/object_detection_cpu +#INFO: Elapsed time: 16.020s, Forge stats: 13001/13003 actions cached, 2.1s CPU used, 0.0s queue time, 89.0 MB ObjFS output (novel bytes: 88.0 MB), 0.0 MB local output, Critical Path: 10.01s, Remote (41.42% of the time): [queue: 0.00%, setup: 4.21%, process: 12.48%] +#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 \ + --calculator_graph_config_file=mediapipe/graphs/object_detection/object_detection_desktop_live.pbtxt +``` + #### Graph ![graph visualization](images/object_detection_desktop_tflite.png) diff --git a/mediapipe/docs/visualizer.md b/mediapipe/docs/visualizer.md index 86cbfe1e6..9cab5dd4b 100644 --- a/mediapipe/docs/visualizer.md +++ b/mediapipe/docs/visualizer.md @@ -77,7 +77,7 @@ For instance, there are two graphs involved in the [hand detection example](./hand_detection_mobile_gpu.md): the main graph ([source pbtxt file](https://github.com/google/mediapipe/tree/master/mediapipe/graphs/hand_tracking/hand_detection_mobile.pbtxt)) and its associated subgraph -([source pbtxt file](https://github.com/google/mediapipe/tree/master/mediapipe/graphs/hand_tracking/hand_detection_gpu.pbtxt)). +([source pbtxt file](https://github.com/google/mediapipe/tree/master/mediapipe/graphs/hand_tracking/subgraphs/hand_detection_gpu.pbtxt)). To visualize them: * In the MediaPipe visualizer, click on the upload graph button and select the diff --git a/mediapipe/examples/desktop/BUILD b/mediapipe/examples/desktop/BUILD index 829601df7..3a35d724b 100644 --- a/mediapipe/examples/desktop/BUILD +++ b/mediapipe/examples/desktop/BUILD @@ -14,7 +14,9 @@ licenses(["notice"]) # Apache 2.0 -package(default_visibility = ["//mediapipe/examples:__subpackages__"]) +package(default_visibility = [ + "//visibility:public", +]) cc_library( name = "simple_run_graph_main", @@ -29,3 +31,44 @@ cc_library( "@com_google_absl//absl/strings", ], ) + +cc_library( + name = "demo_run_graph_main", + srcs = ["demo_run_graph_main.cc"], + deps = [ + "//mediapipe/framework:calculator_framework", + "//mediapipe/framework/formats:image_frame", + "//mediapipe/framework/formats:image_frame_opencv", + "//mediapipe/framework/port:commandlineflags", + "//mediapipe/framework/port:file_helpers", + "//mediapipe/framework/port:opencv_highgui", + "//mediapipe/framework/port:opencv_imgproc", + "//mediapipe/framework/port:opencv_video", + "//mediapipe/framework/port:parse_text_proto", + "//mediapipe/framework/port:status", + ], +) + +# Linux only. +# Must have a GPU with EGL support: +# ex: sudo aptitude install mesa-common-dev libegl1-mesa-dev libgles2-mesa-dev +# (or similar nvidia/amd equivalent) +cc_library( + name = "demo_run_graph_main_gpu", + srcs = ["demo_run_graph_main_gpu.cc"], + deps = [ + "//mediapipe/framework:calculator_framework", + "//mediapipe/framework/formats:image_frame", + "//mediapipe/framework/formats:image_frame_opencv", + "//mediapipe/framework/port:commandlineflags", + "//mediapipe/framework/port:file_helpers", + "//mediapipe/framework/port:opencv_highgui", + "//mediapipe/framework/port:opencv_imgproc", + "//mediapipe/framework/port:opencv_video", + "//mediapipe/framework/port:parse_text_proto", + "//mediapipe/framework/port:status", + "//mediapipe/gpu:gl_calculator_helper", + "//mediapipe/gpu:gpu_buffer", + "//mediapipe/gpu:gpu_shared_data_internal", + ], +) diff --git a/mediapipe/examples/desktop/demo_run_graph_main.cc b/mediapipe/examples/desktop/demo_run_graph_main.cc new file mode 100644 index 000000000..14136560c --- /dev/null +++ b/mediapipe/examples/desktop/demo_run_graph_main.cc @@ -0,0 +1,146 @@ +// 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. +// +// An example of sending OpenCV webcam frames into a MediaPipe graph. + +#include "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/formats/image_frame.h" +#include "mediapipe/framework/formats/image_frame_opencv.h" +#include "mediapipe/framework/port/commandlineflags.h" +#include "mediapipe/framework/port/file_helpers.h" +#include "mediapipe/framework/port/opencv_highgui_inc.h" +#include "mediapipe/framework/port/opencv_imgproc_inc.h" +#include "mediapipe/framework/port/opencv_video_inc.h" +#include "mediapipe/framework/port/parse_text_proto.h" +#include "mediapipe/framework/port/status.h" + +constexpr char kInputStream[] = "input_video"; +constexpr char kOutputStream[] = "output_video"; +constexpr char kWindowName[] = "MediaPipe"; + +DEFINE_string( + calculator_graph_config_file, "", + "Name of file containing text format CalculatorGraphConfig proto."); +DEFINE_string(input_video_path, "", + "Full path of video to load. " + "If not provided, attempt to use a webcam."); +DEFINE_string(output_video_path, "", + "Full path of where to save result (.mp4 only). " + "If not provided, show result in a window."); + +::mediapipe::Status RunMPPGraph() { + std::string calculator_graph_config_contents; + 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( + calculator_graph_config_contents); + + LOG(INFO) << "Initialize the calculator graph."; + mediapipe::CalculatorGraph graph; + MP_RETURN_IF_ERROR(graph.Initialize(config)); + + LOG(INFO) << "Initialize the camera or load the video."; + cv::VideoCapture capture; + const bool load_video = !FLAGS_input_video_path.empty(); + if (load_video) { + capture.open(FLAGS_input_video_path); + } else { + capture.open(0); + } + RET_CHECK(capture.isOpened()); + + cv::VideoWriter writer; + const bool save_video = !FLAGS_output_video_path.empty(); + if (save_video) { + LOG(INFO) << "Prepare video writer."; + cv::Mat test_frame; + capture.read(test_frame); // Consume first frame. + capture.set(cv::CAP_PROP_POS_AVI_RATIO, 0); // Rewind to beginning. + writer.open(FLAGS_output_video_path, + mediapipe::fourcc('a', 'v', 'c', '1'), // .mp4 + capture.get(cv::CAP_PROP_FPS), test_frame.size()); + RET_CHECK(writer.isOpened()); + } else { + cv::namedWindow(kWindowName, /*flags=WINDOW_AUTOSIZE*/ 1); + } + + LOG(INFO) << "Start running the calculator graph."; + ASSIGN_OR_RETURN(mediapipe::OutputStreamPoller poller, + graph.AddOutputStreamPoller(kOutputStream)); + MP_RETURN_IF_ERROR(graph.StartRun({})); + + LOG(INFO) << "Start grabbing and processing frames."; + size_t frame_timestamp = 0; + bool grab_frames = true; + while (grab_frames) { + // Capture opencv camera or video frame. + cv::Mat camera_frame_raw; + capture >> camera_frame_raw; + if (camera_frame_raw.empty()) break; // End of video. + cv::Mat camera_frame; + cv::cvtColor(camera_frame_raw, camera_frame, cv::COLOR_BGR2RGB); + if (!load_video) { + cv::flip(camera_frame, camera_frame, /*flipcode=HORIZONTAL*/ 1); + } + + // Wrap Mat into an ImageFrame. + auto input_frame = absl::make_unique( + mediapipe::ImageFormat::SRGB, camera_frame.cols, camera_frame.rows, + mediapipe::ImageFrame::kDefaultAlignmentBoundary); + cv::Mat input_frame_mat = mediapipe::formats::MatView(input_frame.get()); + camera_frame.copyTo(input_frame_mat); + + // Send image packet into the graph. + MP_RETURN_IF_ERROR(graph.AddPacketToInputStream( + kInputStream, mediapipe::Adopt(input_frame.release()) + .At(mediapipe::Timestamp(frame_timestamp++)))); + + // Get the graph result packet, or stop if that fails. + mediapipe::Packet packet; + if (!poller.Next(&packet)) break; + auto& output_frame = packet.Get(); + + // Convert back to opencv for display or saving. + cv::Mat output_frame_mat = mediapipe::formats::MatView(&output_frame); + cv::cvtColor(output_frame_mat, output_frame_mat, cv::COLOR_RGB2BGR); + if (save_video) { + writer.write(output_frame_mat); + } else { + cv::imshow(kWindowName, output_frame_mat); + // Press any key to exit. + const int pressed_key = cv::waitKey(5); + if (pressed_key >= 0 && pressed_key != 255) grab_frames = false; + } + } + + LOG(INFO) << "Shutting down."; + if (writer.isOpened()) writer.release(); + MP_RETURN_IF_ERROR(graph.CloseInputStream(kInputStream)); + return graph.WaitUntilDone(); +} + +int main(int argc, char** argv) { + google::InitGoogleLogging(argv[0]); + gflags::ParseCommandLineFlags(&argc, &argv, true); + ::mediapipe::Status run_status = RunMPPGraph(); + if (!run_status.ok()) { + LOG(ERROR) << "Failed to run the graph: " << run_status.message(); + } else { + LOG(INFO) << "Success!"; + } + return 0; +} diff --git a/mediapipe/examples/desktop/demo_run_graph_main_gpu.cc b/mediapipe/examples/desktop/demo_run_graph_main_gpu.cc new file mode 100644 index 000000000..4bf8cf97a --- /dev/null +++ b/mediapipe/examples/desktop/demo_run_graph_main_gpu.cc @@ -0,0 +1,186 @@ +// 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. +// +// An example of sending OpenCV webcam frames into a MediaPipe graph. +// This example requires a linux computer and a GPU with EGL support drivers. + +#include "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/formats/image_frame.h" +#include "mediapipe/framework/formats/image_frame_opencv.h" +#include "mediapipe/framework/port/commandlineflags.h" +#include "mediapipe/framework/port/file_helpers.h" +#include "mediapipe/framework/port/opencv_highgui_inc.h" +#include "mediapipe/framework/port/opencv_imgproc_inc.h" +#include "mediapipe/framework/port/opencv_video_inc.h" +#include "mediapipe/framework/port/parse_text_proto.h" +#include "mediapipe/framework/port/status.h" +#include "mediapipe/gpu/gl_calculator_helper.h" +#include "mediapipe/gpu/gpu_buffer.h" +#include "mediapipe/gpu/gpu_shared_data_internal.h" + +constexpr char kInputStream[] = "input_video"; +constexpr char kOutputStream[] = "output_video"; +constexpr char kWindowName[] = "MediaPipe"; + +DEFINE_string( + calculator_graph_config_file, "", + "Name of file containing text format CalculatorGraphConfig proto."); +DEFINE_string(input_video_path, "", + "Full path of video to load. " + "If not provided, attempt to use a webcam."); +DEFINE_string(output_video_path, "", + "Full path of where to save result (.mp4 only). " + "If not provided, show result in a window."); + +::mediapipe::Status RunMPPGraph() { + std::string calculator_graph_config_contents; + 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( + calculator_graph_config_contents); + + LOG(INFO) << "Initialize the calculator graph."; + mediapipe::CalculatorGraph graph; + MP_RETURN_IF_ERROR(graph.Initialize(config)); + + LOG(INFO) << "Initialize the GPU."; + ASSIGN_OR_RETURN(auto gpu_resources, mediapipe::GpuResources::Create()); + MP_RETURN_IF_ERROR(graph.SetGpuResources(std::move(gpu_resources))); + mediapipe::GlCalculatorHelper gpu_helper; + gpu_helper.InitializeForTest(graph.GetGpuResources().get()); + + LOG(INFO) << "Initialize the camera or load the video."; + cv::VideoCapture capture; + const bool load_video = !FLAGS_input_video_path.empty(); + if (load_video) { + capture.open(FLAGS_input_video_path); + } else { + capture.open(0); + } + RET_CHECK(capture.isOpened()); + + cv::VideoWriter writer; + const bool save_video = !FLAGS_output_video_path.empty(); + if (save_video) { + LOG(INFO) << "Prepare video writer."; + cv::Mat test_frame; + capture.read(test_frame); // Consume first frame. + capture.set(cv::CAP_PROP_POS_AVI_RATIO, 0); // Rewind to beginning. + writer.open(FLAGS_output_video_path, + mediapipe::fourcc('a', 'v', 'c', '1'), // .mp4 + capture.get(cv::CAP_PROP_FPS), test_frame.size()); + RET_CHECK(writer.isOpened()); + } else { + cv::namedWindow(kWindowName, /*flags=WINDOW_AUTOSIZE*/ 1); + } + + LOG(INFO) << "Start running the calculator graph."; + ASSIGN_OR_RETURN(mediapipe::OutputStreamPoller poller, + graph.AddOutputStreamPoller(kOutputStream)); + MP_RETURN_IF_ERROR(graph.StartRun({})); + + LOG(INFO) << "Start grabbing and processing frames."; + size_t frame_timestamp = 0; + bool grab_frames = true; + while (grab_frames) { + // Capture opencv camera or video frame. + cv::Mat camera_frame_raw; + capture >> camera_frame_raw; + if (camera_frame_raw.empty()) break; // End of video. + cv::Mat camera_frame; + cv::cvtColor(camera_frame_raw, camera_frame, cv::COLOR_BGR2RGB); + if (!load_video) { + cv::flip(camera_frame, camera_frame, /*flipcode=HORIZONTAL*/ 1); + } + + // Wrap Mat into an ImageFrame. + auto input_frame = absl::make_unique( + mediapipe::ImageFormat::SRGB, camera_frame.cols, camera_frame.rows, + mediapipe::ImageFrame::kGlDefaultAlignmentBoundary); + cv::Mat input_frame_mat = mediapipe::formats::MatView(input_frame.get()); + camera_frame.copyTo(input_frame_mat); + + // Prepare and add graph input packet. + MP_RETURN_IF_ERROR( + gpu_helper.RunInGlContext([&input_frame, &frame_timestamp, &graph, + &gpu_helper]() -> ::mediapipe::Status { + // Convert ImageFrame to GpuBuffer. + auto texture = gpu_helper.CreateSourceTexture(*input_frame.get()); + auto gpu_frame = texture.GetFrame(); + glFlush(); + texture.Release(); + // Send GPU image packet into the graph. + MP_RETURN_IF_ERROR(graph.AddPacketToInputStream( + kInputStream, mediapipe::Adopt(gpu_frame.release()) + .At(mediapipe::Timestamp(frame_timestamp++)))); + return ::mediapipe::OkStatus(); + })); + + // Get the graph result packet, or stop if that fails. + mediapipe::Packet packet; + if (!poller.Next(&packet)) break; + std::unique_ptr output_frame; + + // Convert GpuBuffer to ImageFrame. + MP_RETURN_IF_ERROR(gpu_helper.RunInGlContext( + [&packet, &output_frame, &gpu_helper]() -> ::mediapipe::Status { + auto& gpu_frame = packet.Get(); + auto texture = gpu_helper.CreateSourceTexture(gpu_frame); + output_frame = absl::make_unique( + mediapipe::ImageFormatForGpuBufferFormat(gpu_frame.format()), + gpu_frame.width(), gpu_frame.height(), + mediapipe::ImageFrame::kGlDefaultAlignmentBoundary); + gpu_helper.BindFramebuffer(texture); + const auto info = + mediapipe::GlTextureInfoForGpuBufferFormat(gpu_frame.format(), 0); + glReadPixels(0, 0, texture.width(), texture.height(), info.gl_format, + info.gl_type, output_frame->MutablePixelData()); + glFlush(); + texture.Release(); + return ::mediapipe::OkStatus(); + })); + + // Convert back to opencv for display or saving. + cv::Mat output_frame_mat = mediapipe::formats::MatView(output_frame.get()); + cv::cvtColor(output_frame_mat, output_frame_mat, cv::COLOR_RGB2BGR); + if (save_video) { + writer.write(output_frame_mat); + } else { + cv::imshow(kWindowName, output_frame_mat); + // Press any key to exit. + const int pressed_key = cv::waitKey(5); + if (pressed_key >= 0 && pressed_key != 255) grab_frames = false; + } + } + + LOG(INFO) << "Shutting down."; + if (writer.isOpened()) writer.release(); + MP_RETURN_IF_ERROR(graph.CloseInputStream(kInputStream)); + return graph.WaitUntilDone(); +} + +int main(int argc, char** argv) { + google::InitGoogleLogging(argv[0]); + gflags::ParseCommandLineFlags(&argc, &argv, true); + ::mediapipe::Status run_status = RunMPPGraph(); + if (!run_status.ok()) { + LOG(ERROR) << "Failed to run the graph: " << run_status.message(); + } else { + LOG(INFO) << "Success!"; + } + return 0; +} diff --git a/mediapipe/examples/desktop/face_detection/BUILD b/mediapipe/examples/desktop/face_detection/BUILD new file mode 100644 index 000000000..3d1dbcec8 --- /dev/null +++ b/mediapipe/examples/desktop/face_detection/BUILD @@ -0,0 +1,34 @@ +# 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 + +package(default_visibility = ["//mediapipe/examples:__subpackages__"]) + +cc_binary( + name = "face_detection_cpu", + deps = [ + "//mediapipe/examples/desktop:demo_run_graph_main", + "//mediapipe/graphs/face_detection:desktop_tflite_calculators", + ], +) + +# Linux only +cc_binary( + name = "face_detection_gpu", + deps = [ + "//mediapipe/examples/desktop:demo_run_graph_main_gpu", + "//mediapipe/graphs/face_detection:mobile_calculators", + ], +) diff --git a/mediapipe/examples/desktop/hair_segmentation/BUILD b/mediapipe/examples/desktop/hair_segmentation/BUILD new file mode 100644 index 000000000..0338feddf --- /dev/null +++ b/mediapipe/examples/desktop/hair_segmentation/BUILD @@ -0,0 +1,26 @@ +# 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 + +package(default_visibility = ["//mediapipe/examples:__subpackages__"]) + +# Linux only +cc_binary( + name = "hair_segmentation_gpu", + deps = [ + "//mediapipe/examples/desktop:demo_run_graph_main_gpu", + "//mediapipe/graphs/hair_segmentation:mobile_calculators", + ], +) diff --git a/mediapipe/examples/desktop/hand_tracking/BUILD b/mediapipe/examples/desktop/hand_tracking/BUILD new file mode 100644 index 000000000..1c99b00f6 --- /dev/null +++ b/mediapipe/examples/desktop/hand_tracking/BUILD @@ -0,0 +1,42 @@ +# 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 + +package(default_visibility = ["//mediapipe/examples:__subpackages__"]) + +cc_binary( + name = "hand_tracking_tflite", + deps = [ + "//mediapipe/examples/desktop:simple_run_graph_main", + "//mediapipe/graphs/hand_tracking:desktop_tflite_calculators", + ], +) + +cc_binary( + name = "hand_tracking_cpu", + deps = [ + "//mediapipe/examples/desktop:demo_run_graph_main", + "//mediapipe/graphs/hand_tracking:desktop_tflite_calculators", + ], +) + +# Linux only +cc_binary( + name = "hand_tracking_gpu", + deps = [ + "//mediapipe/examples/desktop:demo_run_graph_main_gpu", + "//mediapipe/graphs/hand_tracking:mobile_calculators", + ], +) diff --git a/mediapipe/examples/desktop/object_detection/BUILD b/mediapipe/examples/desktop/object_detection/BUILD index 4ce4fd900..ee6832069 100644 --- a/mediapipe/examples/desktop/object_detection/BUILD +++ b/mediapipe/examples/desktop/object_detection/BUILD @@ -72,3 +72,11 @@ cc_binary( "//mediapipe/graphs/object_detection:desktop_tflite_calculators", ], ) + +cc_binary( + name = "object_detection_cpu", + deps = [ + "//mediapipe/examples/desktop:demo_run_graph_main", + "//mediapipe/graphs/object_detection:desktop_tflite_calculators", + ], +) diff --git a/mediapipe/examples/desktop/youtube8m/BUILD b/mediapipe/examples/desktop/youtube8m/BUILD index 8c97ffc8c..c25c5f50d 100644 --- a/mediapipe/examples/desktop/youtube8m/BUILD +++ b/mediapipe/examples/desktop/youtube8m/BUILD @@ -27,7 +27,7 @@ cc_binary( "//mediapipe/framework/port:map_util", "//mediapipe/framework/port:parse_text_proto", "//mediapipe/framework/port:status", - "//mediapipe/graphs/youtube8m:yt8m_calculators_deps", + "//mediapipe/graphs/youtube8m:yt8m_feature_extraction_calculators", # 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 b777cd789..2989a7927 100644 --- a/mediapipe/examples/desktop/youtube8m/README.md +++ b/mediapipe/examples/desktop/youtube8m/README.md @@ -37,7 +37,9 @@ ```bash 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_start_time_sec=0 \ + --clip_end_time_sec=10 ``` 5. Run the MediaPipe binary to extract the features diff --git a/mediapipe/examples/desktop/youtube8m/generate_input_sequence_example.py b/mediapipe/examples/desktop/youtube8m/generate_input_sequence_example.py index b814f9ea4..7438a5134 100644 --- a/mediapipe/examples/desktop/youtube8m/generate_input_sequence_example.py +++ b/mediapipe/examples/desktop/youtube8m/generate_input_sequence_example.py @@ -37,20 +37,29 @@ def bytes23(string): def main(argv): - if len(argv) > 1: + if len(argv) > 3: raise app.UsageError('Too many command-line arguments.') if not flags.FLAGS.path_to_input_video: raise ValueError('You must specify the path to the input video.') + if not flags.FLAGS.clip_end_time_sec: + raise ValueError('You must specify the clip end timestamp in seconds.') + if flags.FLAGS.clip_start_time_sec >= flags.FLAGS.clip_end_time_sec: + raise ValueError( + 'The clip start time must be greater than the clip end time.') metadata = tf.train.SequenceExample() ms.set_clip_data_path(bytes23(flags.FLAGS.path_to_input_video), metadata) - ms.set_clip_start_timestamp(0, metadata) + ms.set_clip_start_timestamp( + flags.FLAGS.clip_start_time_sec * SECONDS_TO_MICROSECONDS, metadata) ms.set_clip_end_timestamp( - int(float(300 * SECONDS_TO_MICROSECONDS)), metadata) + flags.FLAGS.clip_end_time_sec * SECONDS_TO_MICROSECONDS, metadata) with open('/tmp/mediapipe/metadata.tfrecord', 'wb') as writer: writer.write(metadata.SerializeToString()) if __name__ == '__main__': flags.DEFINE_string('path_to_input_video', '', 'Path to the input video.') + flags.DEFINE_integer('clip_start_time_sec', 0, + 'Clip start timestamp in seconds') + flags.DEFINE_integer('clip_end_time_sec', 10, 'Clip end timestamp in seconds') app.run(main) diff --git a/mediapipe/framework/BUILD b/mediapipe/framework/BUILD index bc83ebcd8..90a4f672c 100644 --- a/mediapipe/framework/BUILD +++ b/mediapipe/framework/BUILD @@ -1134,6 +1134,7 @@ cc_library( "//mediapipe/framework/port:status", "//mediapipe/framework/port:statusor", "//mediapipe/framework/tool:calculator_graph_template_cc_proto", + "//mediapipe/framework/tool:options_util", "//mediapipe/framework/tool:template_expander", "@com_google_absl//absl/base:core_headers", "@com_google_absl//absl/memory", @@ -1499,13 +1500,47 @@ cc_test( deps = [ ":calculator_context", ":calculator_framework", + ":test_calculators", + ":thread_pool_executor", ":timestamp", + ":type_map", + "//mediapipe/calculators/core:counting_source_calculator", + "//mediapipe/calculators/core:mux_calculator", "//mediapipe/calculators/core:pass_through_calculator", "//mediapipe/framework/port:gtest_main", "//mediapipe/framework/port:logging", "//mediapipe/framework/port:parse_text_proto", "//mediapipe/framework/port:status", + "//mediapipe/framework/stream_handler:barrier_input_stream_handler", + "//mediapipe/framework/stream_handler:early_close_input_stream_handler", + "//mediapipe/framework/stream_handler:fixed_size_input_stream_handler", "//mediapipe/framework/stream_handler:immediate_input_stream_handler", + "//mediapipe/framework/stream_handler:mux_input_stream_handler", + "//mediapipe/framework/tool:sink", + ], +) + +cc_test( + name = "calculator_graph_side_packet_test", + size = "small", + srcs = [ + "calculator_graph_side_packet_test.cc", + ], + visibility = ["//visibility:public"], + deps = [ + ":calculator_framework", + ":test_calculators", + "//mediapipe/calculators/core:counting_source_calculator", + "//mediapipe/calculators/core:mux_calculator", + "//mediapipe/calculators/core:pass_through_calculator", + "//mediapipe/framework:calculator_cc_proto", + "//mediapipe/framework/port:gtest_main", + "//mediapipe/framework/port:logging", + "//mediapipe/framework/port:parse_text_proto", + "//mediapipe/framework/port:ret_check", + "//mediapipe/framework/port:status", + "//mediapipe/framework/tool:sink", + "@com_google_absl//absl/time", ], ) diff --git a/mediapipe/framework/calculator_graph_bounds_test.cc b/mediapipe/framework/calculator_graph_bounds_test.cc index 750042522..1b8c3e9f2 100644 --- a/mediapipe/framework/calculator_graph_bounds_test.cc +++ b/mediapipe/framework/calculator_graph_bounds_test.cc @@ -21,11 +21,258 @@ #include "mediapipe/framework/port/parse_text_proto.h" #include "mediapipe/framework/port/status.h" #include "mediapipe/framework/port/status_matchers.h" +#include "mediapipe/framework/thread_pool_executor.h" #include "mediapipe/framework/timestamp.h" namespace mediapipe { namespace { +typedef std::function<::mediapipe::Status(CalculatorContext* cc)> + CalculatorContextFunction; + +// A simple Semaphore for synchronizing test threads. +class AtomicSemaphore { + public: + AtomicSemaphore(int64_t supply) : supply_(supply) {} + void Acquire(int64_t amount) { + while (supply_.fetch_sub(amount) - amount < 0) { + Release(amount); + } + } + void Release(int64_t amount) { supply_ += amount; } + + private: + std::atomic supply_; +}; + +// A mediapipe::Executor that signals the start and finish of each task. +class CountingExecutor : public Executor { + public: + CountingExecutor(int num_threads, std::function start_callback, + std::function finish_callback) + : thread_pool_(num_threads), + start_callback_(std::move(start_callback)), + finish_callback_(std::move(finish_callback)) { + thread_pool_.StartWorkers(); + } + void Schedule(std::function task) override { + start_callback_(); + thread_pool_.Schedule([this, task] { + task(); + finish_callback_(); + }); + } + + private: + ThreadPool thread_pool_; + std::function start_callback_; + std::function finish_callback_; +}; + +// A Calculator that adds the integer values in the packets in all the input +// streams and outputs the sum to the output stream. +class IntAdderCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + for (int i = 0; i < cc->Inputs().NumEntries(); ++i) { + cc->Inputs().Index(i).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 { + int sum = 0; + for (int i = 0; i < cc->Inputs().NumEntries(); ++i) { + sum += cc->Inputs().Index(i).Get(); + } + cc->Outputs().Index(0).Add(new int(sum), cc->InputTimestamp()); + return ::mediapipe::OkStatus(); + } +}; +REGISTER_CALCULATOR(IntAdderCalculator); + +template +class TypedSinkCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->Inputs().Index(0).Set(); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) override { + return ::mediapipe::OkStatus(); + } +}; +typedef TypedSinkCalculator StringSinkCalculator; +typedef TypedSinkCalculator IntSinkCalculator; +REGISTER_CALCULATOR(StringSinkCalculator); +REGISTER_CALCULATOR(IntSinkCalculator); + +// A Calculator that passes an input packet through if it contains an even +// integer. +class EvenIntFilterCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->Inputs().Index(0).Set(); + cc->Outputs().Index(0).Set(); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) final { + int value = cc->Inputs().Index(0).Get(); + if (value % 2 == 0) { + cc->Outputs().Index(0).AddPacket(cc->Inputs().Index(0).Value()); + } else { + cc->Outputs().Index(0).SetNextTimestampBound( + cc->InputTimestamp().NextAllowedInStream()); + } + return ::mediapipe::OkStatus(); + } +}; +REGISTER_CALCULATOR(EvenIntFilterCalculator); + +// A Calculator that passes packets through or not, depending on a second +// input. The first input stream's packets are only propagated if the second +// input stream carries the value true. +class ValveCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->Inputs().Index(0).SetAny(); + cc->Inputs().Index(1).Set(); + cc->Outputs().Index(0).SetSameAs(&cc->Inputs().Index(0)); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Open(CalculatorContext* cc) final { + cc->Outputs().Index(0).SetHeader(cc->Inputs().Index(0).Header()); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) final { + if (cc->Inputs().Index(1).Get()) { + cc->GetCounter("PassThrough")->Increment(); + cc->Outputs().Index(0).AddPacket(cc->Inputs().Index(0).Value()); + } else { + cc->GetCounter("Block")->Increment(); + // The next timestamp bound is the minimum timestamp that the next packet + // can have, so, if we want to inform the downstream that no packet at + // InputTimestamp() is coming, we need to set it to the next value. + // We could also just call SetOffset(TimestampDiff(0)) in Open, and then + // we would not have to call this manually. + cc->Outputs().Index(0).SetNextTimestampBound( + cc->InputTimestamp().NextAllowedInStream()); + } + return ::mediapipe::OkStatus(); + } +}; +REGISTER_CALCULATOR(ValveCalculator); + +// A Calculator that simply passes its input Packets and header through, +// but shifts the timestamp. +class TimeShiftCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->Inputs().Index(0).SetAny(); + cc->Outputs().Index(0).SetSameAs(&cc->Inputs().Index(0)); + cc->InputSidePackets().Index(0).Set(); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Open(CalculatorContext* cc) final { + // Input: arbitrary Packets. + // Output: copy of the input. + cc->Outputs().Index(0).SetHeader(cc->Inputs().Index(0).Header()); + shift_ = cc->InputSidePackets().Index(0).Get(); + cc->SetOffset(shift_); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) final { + cc->GetCounter("PassThrough")->Increment(); + cc->Outputs().Index(0).AddPacket( + cc->Inputs().Index(0).Value().At(cc->InputTimestamp() + shift_)); + return ::mediapipe::OkStatus(); + } + + private: + TimestampDiff shift_; +}; +REGISTER_CALCULATOR(TimeShiftCalculator); + +// A source calculator that alternates between outputting an integer (0, 1, 2, +// ..., 100) and setting the next timestamp bound. The timestamps of the output +// packets and next timestamp bounds are 0, 10, 20, 30, ... +// +// T=0 Output 0 +// T=10 Set timestamp bound +// T=20 Output 1 +// T=30 Set timestamp bound +// ... +// T=2000 Output 100 +class OutputAndBoundSourceCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->Outputs().Index(0).Set(); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Open(CalculatorContext* cc) override { + counter_ = 0; + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) override { + Timestamp timestamp(counter_); + if (counter_ % 20 == 0) { + cc->Outputs().Index(0).AddPacket( + MakePacket(counter_ / 20).At(timestamp)); + } else { + cc->Outputs().Index(0).SetNextTimestampBound(timestamp); + } + if (counter_ == 2000) { + return tool::StatusStop(); + } + counter_ += 10; + return ::mediapipe::OkStatus(); + } + + private: + int counter_; +}; +REGISTER_CALCULATOR(OutputAndBoundSourceCalculator); + +// A calculator that outputs an initial packet of value 0 at time 0 in the +// Open() method, and then delays each input packet by 20 time units in the +// Process() method. The input stream and output stream have the integer type. +class Delay20Calculator : 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(20)); + cc->Outputs().Index(0).AddPacket(MakePacket(0).At(Timestamp(0))); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) final { + const Packet& packet = cc->Inputs().Index(0).Value(); + Timestamp timestamp = packet.Timestamp() + 20; + cc->Outputs().Index(0).AddPacket(packet.At(timestamp)); + return ::mediapipe::OkStatus(); + } +}; +REGISTER_CALCULATOR(Delay20Calculator); + class CustomBoundCalculator : public CalculatorBase { public: static ::mediapipe::Status GetContract(CalculatorContract* cc) { @@ -45,8 +292,280 @@ class CustomBoundCalculator : public CalculatorBase { }; REGISTER_CALCULATOR(CustomBoundCalculator); +// Test that SetNextTimestampBound propagates. +TEST(CalculatorGraph, SetNextTimestampBoundPropagation) { + CalculatorGraph graph; + CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie(R"( + input_stream: 'in' + input_stream: 'gate' + node { + calculator: 'ValveCalculator' + input_stream: 'in' + input_stream: 'gate' + output_stream: 'gated' + } + node { + calculator: 'PassThroughCalculator' + input_stream: 'gated' + output_stream: 'passed' + } + node { + calculator: 'TimeShiftCalculator' + input_stream: 'passed' + output_stream: 'shifted' + input_side_packet: 'shift' + } + node { + calculator: 'MergeCalculator' + input_stream: 'in' + input_stream: 'shifted' + output_stream: 'merged' + } + node { + name: 'merged_output' + calculator: 'PassThroughCalculator' + input_stream: 'merged' + output_stream: 'out' + } + )"); + + Timestamp timestamp = Timestamp(0); + auto send_inputs = [&graph, ×tamp](int input, bool pass) { + ++timestamp; + MP_EXPECT_OK(graph.AddPacketToInputStream( + "in", MakePacket(input).At(timestamp))); + MP_EXPECT_OK(graph.AddPacketToInputStream( + "gate", MakePacket(pass).At(timestamp))); + }; + + MP_ASSERT_OK(graph.Initialize(config)); + MP_ASSERT_OK(graph.StartRun({{"shift", MakePacket(0)}})); + + auto pass_counter = + graph.GetCounterFactory()->GetCounter("ValveCalculator-PassThrough"); + auto block_counter = + graph.GetCounterFactory()->GetCounter("ValveCalculator-Block"); + auto merged_counter = + graph.GetCounterFactory()->GetCounter("merged_output-PassThrough"); + + send_inputs(1, true); + send_inputs(2, true); + send_inputs(3, false); + send_inputs(4, false); + MP_ASSERT_OK(graph.WaitUntilIdle()); + + // Verify that MergeCalculator was able to run even when the gated branch + // was blocked. + EXPECT_EQ(2, pass_counter->Get()); + EXPECT_EQ(2, block_counter->Get()); + EXPECT_EQ(4, merged_counter->Get()); + + send_inputs(5, true); + send_inputs(6, false); + MP_ASSERT_OK(graph.WaitUntilIdle()); + + EXPECT_EQ(3, pass_counter->Get()); + EXPECT_EQ(3, block_counter->Get()); + EXPECT_EQ(6, merged_counter->Get()); + + MP_ASSERT_OK(graph.CloseAllInputStreams()); + MP_ASSERT_OK(graph.WaitUntilDone()); + + // Now test with time shift + MP_ASSERT_OK(graph.StartRun({{"shift", MakePacket(-1)}})); + + send_inputs(7, true); + MP_ASSERT_OK(graph.WaitUntilIdle()); + + // The merger should have run only once now, at timestamp 6, with inputs + // . If we do not respect the offset and unblock the merger for + // timestamp 7 too, then it will have run twice, with 6: and + // 7: <7, null>. + EXPECT_EQ(4, pass_counter->Get()); + EXPECT_EQ(3, block_counter->Get()); + EXPECT_EQ(7, merged_counter->Get()); + + MP_ASSERT_OK(graph.CloseAllInputStreams()); + MP_ASSERT_OK(graph.WaitUntilDone()); + + EXPECT_EQ(4, pass_counter->Get()); + EXPECT_EQ(3, block_counter->Get()); + EXPECT_EQ(8, merged_counter->Get()); +} + +// Both input streams of the calculator node have the same next timestamp +// bound. One input stream has a packet at that timestamp. The other input +// stream is empty. We should not run the Process() method of the node in this +// case. +TEST(CalculatorGraph, NotAllInputPacketsAtNextTimestampBoundAvailable) { + // + // in0_unfiltered in1_to_be_filtered + // | | + // | V + // | +-----------------------+ + // | |EvenIntFilterCalculator| + // | +-----------------------+ + // | | + // \ / + // \ / in1_filtered + // \ / + // | | + // V V + // +------------------+ + // |IntAdderCalculator| + // +------------------+ + // | + // V + // out + // + CalculatorGraph graph; + CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie(R"( + input_stream: 'in0_unfiltered' + input_stream: 'in1_to_be_filtered' + node { + calculator: 'EvenIntFilterCalculator' + input_stream: 'in1_to_be_filtered' + output_stream: 'in1_filtered' + } + node { + calculator: 'IntAdderCalculator' + input_stream: 'in0_unfiltered' + input_stream: 'in1_filtered' + output_stream: 'out' + } + )"); + std::vector packet_dump; + tool::AddVectorSink("out", &config, &packet_dump); + + MP_ASSERT_OK(graph.Initialize(config)); + MP_ASSERT_OK(graph.StartRun({})); + + Timestamp timestamp = Timestamp(0); + + // We send an integer with timestamp 1 to the in0_unfiltered input stream of + // the IntAdderCalculator. We then send an even integer with timestamp 1 to + // the EvenIntFilterCalculator. This packet will go through and + // the IntAdderCalculator will run. The next timestamp bounds of both the + // input streams of the IntAdderCalculator will become 2. + + ++timestamp; // Timestamp 1. + MP_EXPECT_OK(graph.AddPacketToInputStream("in0_unfiltered", + MakePacket(1).At(timestamp))); + MP_EXPECT_OK(graph.AddPacketToInputStream("in1_to_be_filtered", + MakePacket(2).At(timestamp))); + MP_ASSERT_OK(graph.WaitUntilIdle()); + ASSERT_EQ(1, packet_dump.size()); + EXPECT_EQ(3, packet_dump[0].Get()); + + // We send an odd integer with timestamp 2 to the EvenIntFilterCalculator. + // This packet will be filtered out and the next timestamp bound of the + // in1_filtered input stream of the IntAdderCalculator will become 3. + + ++timestamp; // Timestamp 2. + MP_EXPECT_OK(graph.AddPacketToInputStream("in1_to_be_filtered", + MakePacket(3).At(timestamp))); + + // We send an integer with timestamp 3 to the in0_unfiltered input stream of + // the IntAdderCalculator. MediaPipe should propagate the next timestamp bound + // across the IntAdderCalculator but should not run its Process() method. + + ++timestamp; // Timestamp 3. + MP_EXPECT_OK(graph.AddPacketToInputStream("in0_unfiltered", + MakePacket(3).At(timestamp))); + MP_ASSERT_OK(graph.WaitUntilIdle()); + ASSERT_EQ(1, packet_dump.size()); + + // We send an even integer with timestamp 3 to the IntAdderCalculator. This + // packet will go through and the IntAdderCalculator will run. + + MP_EXPECT_OK(graph.AddPacketToInputStream("in1_to_be_filtered", + MakePacket(4).At(timestamp))); + MP_ASSERT_OK(graph.WaitUntilIdle()); + ASSERT_EQ(2, packet_dump.size()); + EXPECT_EQ(7, packet_dump[1].Get()); + + MP_ASSERT_OK(graph.CloseAllInputStreams()); + MP_ASSERT_OK(graph.WaitUntilDone()); + EXPECT_EQ(2, packet_dump.size()); +} + +TEST(CalculatorGraph, PropagateBoundLoop) { + CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie(R"( + node { + calculator: 'OutputAndBoundSourceCalculator' + output_stream: 'integers' + } + node { + calculator: 'IntAdderCalculator' + input_stream: 'integers' + input_stream: 'old_sum' + input_stream_info: { + tag_index: ':1' # 'old_sum' + back_edge: true + } + output_stream: 'sum' + input_stream_handler { + input_stream_handler: 'EarlyCloseInputStreamHandler' + } + } + node { + calculator: 'Delay20Calculator' + input_stream: 'sum' + output_stream: 'old_sum' + } + )"); + std::vector packet_dump; + tool::AddVectorSink("sum", &config, &packet_dump); + + CalculatorGraph graph; + MP_ASSERT_OK(graph.Initialize(config)); + MP_ASSERT_OK(graph.Run()); + ASSERT_EQ(101, packet_dump.size()); + int sum = 0; + for (int i = 0; i < 101; ++i) { + sum += i; + EXPECT_EQ(sum, packet_dump[i].Get()); + EXPECT_EQ(Timestamp(i * 20), packet_dump[i].Timestamp()); + } +} + +TEST(CalculatorGraph, CheckBatchProcessingBoundPropagation) { + // The timestamp bound sent by OutputAndBoundSourceCalculator shouldn't be + // directly propagated to the output stream when PassThroughCalculator has + // anything in its default calculator context for batch processing. Otherwise, + // the sink calculator's input stream should report packet timestamp + // mismatches. + CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie(R"( + node { + calculator: 'OutputAndBoundSourceCalculator' + output_stream: 'integers' + } + node { + calculator: 'PassThroughCalculator' + input_stream: 'integers' + output_stream: 'output' + input_stream_handler { + input_stream_handler: "DefaultInputStreamHandler" + options: { + [mediapipe.DefaultInputStreamHandlerOptions.ext]: { + batch_size: 10 + } + } + } + } + node { calculator: 'IntSinkCalculator' input_stream: 'output' } + )"); + CalculatorGraph graph; + MP_ASSERT_OK(graph.Initialize(config)); + MP_ASSERT_OK(graph.Run()); +} + // Shows that ImmediateInputStreamHandler allows bounds propagation. -TEST(CalculatorGraphBounds, ImmediateHandlerBounds) { +TEST(CalculatorGraphBoundsTest, ImmediateHandlerBounds) { // CustomBoundCalculator produces only timestamp bounds. // The first PassThroughCalculator propagates bounds using SetOffset(0). // The second PassthroughCalculator delivers an output packet whenever the @@ -101,5 +620,261 @@ TEST(CalculatorGraphBounds, ImmediateHandlerBounds) { EXPECT_EQ(output_packets.size(), 4); } +// A Calculator that only sets timestamp bound by SetOffset(). +class OffsetBoundCalculator : 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(0); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) final { + return ::mediapipe::OkStatus(); + } +}; +REGISTER_CALCULATOR(OffsetBoundCalculator); + +// A Calculator that produces a packet for each call to Process. +class BoundToPacketCalculator : 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 { + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) final { + cc->Outputs().Index(0).AddPacket(Adopt(new int(33))); + return ::mediapipe::OkStatus(); + } +}; +REGISTER_CALCULATOR(BoundToPacketCalculator); + +// Verifies that SetOffset still propagates when Process is called and +// produces no output packets. +TEST(CalculatorGraphBoundsTest, OffsetBoundPropagation) { + // OffsetBoundCalculator produces only timestamp bounds. + // The PassthroughCalculator delivers an output packet whenever the + // OffsetBoundCalculator delivers a timestamp bound. + CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie(R"( + input_stream: 'input' + node { + calculator: 'OffsetBoundCalculator' + input_stream: 'input' + output_stream: 'bounds' + } + node { + calculator: 'PassThroughCalculator' + input_stream: 'bounds' + input_stream: 'input' + output_stream: 'bounds_output' + output_stream: '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(); + })); + 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)); + } + + // Four packets arrive at the output only if timestamp bounds are propagated. + MP_ASSERT_OK(graph.WaitUntilIdle()); + EXPECT_EQ(output_packets.size(), kNumInputs); + + // Shutdown the graph. + MP_ASSERT_OK(graph.CloseAllPacketSources()); + MP_ASSERT_OK(graph.WaitUntilDone()); +} + +// Shows that bounds changes alone do not invoke Process. +// Note: Bounds changes alone will invoke Process eventually +// when SetOffset is cleared, see: go/mediapipe-realtime-graph. +TEST(CalculatorGraphBoundsTest, BoundWithoutInputPackets) { + // OffsetBoundCalculator produces only timestamp bounds. + // The BoundToPacketCalculator delivers an output packet whenever the + // OffsetBoundCalculator delivers a timestamp bound. + CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie(R"( + input_stream: 'input' + node { + calculator: 'OffsetBoundCalculator' + input_stream: 'input' + output_stream: 'bounds' + } + node { + calculator: 'BoundToPacketCalculator' + input_stream: 'bounds' + output_stream: '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(); + })); + 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)); + } + + // No packets arrive, because updated timestamp bounds do not invoke + // BoundToPacketCalculator::Process. + MP_ASSERT_OK(graph.WaitUntilIdle()); + EXPECT_EQ(output_packets.size(), 0); + + // Shutdown the graph. + MP_ASSERT_OK(graph.CloseAllPacketSources()); + MP_ASSERT_OK(graph.WaitUntilDone()); +} + +// Shows that when fixed-size-input-stream-hanlder drops packets, +// no timetamp bounds are announced. +TEST(CalculatorGraphBoundsTest, FixedSizeHandlerBounds) { + // LambdaCalculator with FixedSizeInputStreamHandler will drop packets + // while it is busy. Timetamps for the dropped packets are only relevant + // when SetOffset is active on the LambdaCalculator. + // The PassthroughCalculator delivers an output packet whenever the + // LambdaCalculator delivers a timestamp bound. + CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie(R"( + input_stream: 'input' + input_side_packet: 'open_function' + input_side_packet: 'process_function' + node { + calculator: 'LambdaCalculator' + input_stream: 'input' + output_stream: 'thinned' + input_side_packet: 'OPEN:open_fn' + input_side_packet: 'PROCESS:process_fn' + input_stream_handler { + input_stream_handler: "FixedSizeInputStreamHandler" + } + } + node { + calculator: 'PassThroughCalculator' + input_stream: 'thinned' + input_stream: 'input' + output_stream: 'thinned_output' + output_stream: 'output' + } + )"); + CalculatorGraph graph; + + // The task_semaphore counts the number of running tasks. + constexpr int kTaskSupply = 10; + AtomicSemaphore task_semaphore(/*supply=*/kTaskSupply); + + // This executor invokes a callback at the start and finish of each task. + auto executor = std::make_shared( + 4, /*start_callback=*/[&]() { task_semaphore.Acquire(1); }, + /*finish_callback=*/[&]() { task_semaphore.Release(1); }); + MP_ASSERT_OK(graph.SetExecutor(/*name=*/"", executor)); + + // Monitor output from the graph. + MP_ASSERT_OK(graph.Initialize(config)); + std::vector outputs; + MP_ASSERT_OK(graph.ObserveOutputStream("output", [&](const Packet& p) { + outputs.push_back(p); + return ::mediapipe::OkStatus(); + })); + std::vector thinned_outputs; + MP_ASSERT_OK( + graph.ObserveOutputStream("thinned_output", [&](const Packet& p) { + thinned_outputs.push_back(p); + return ::mediapipe::OkStatus(); + })); + + // The enter_semaphore is used to wait for LambdaCalculator::Process. + // The exit_semaphore blocks and unblocks LambdaCalculator::Process. + AtomicSemaphore enter_semaphore(0); + AtomicSemaphore exit_semaphore(0); + CalculatorContextFunction open_fn = [&](CalculatorContext* cc) { + cc->SetOffset(0); + return ::mediapipe::OkStatus(); + }; + CalculatorContextFunction process_fn = [&](CalculatorContext* cc) { + enter_semaphore.Release(1); + exit_semaphore.Acquire(1); + cc->Outputs().Index(0).AddPacket(cc->Inputs().Index(0).Value()); + return ::mediapipe::OkStatus(); + }; + MP_ASSERT_OK(graph.StartRun({ + {"open_fn", Adopt(new auto(open_fn))}, + {"process_fn", Adopt(new auto(process_fn))}, + })); + 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)); + } + + // Wait until only the LambdaCalculator is running, + // by wating until the task_semaphore has only one token occupied. + // At this point 2 packets were dropped by the FixedSizeInputStreamHandler. + task_semaphore.Acquire(kTaskSupply - 1); + task_semaphore.Release(kTaskSupply - 1); + + // No timestamp bounds and no packets are emitted yet. + EXPECT_EQ(outputs.size(), 0); + EXPECT_EQ(thinned_outputs.size(), 0); + + // Allow the first LambdaCalculator::Process call to complete. + // Wait for the second LambdaCalculator::Process call to begin. + // Wait until only the LambdaCalculator is running. + enter_semaphore.Acquire(1); + exit_semaphore.Release(1); + enter_semaphore.Acquire(1); + task_semaphore.Acquire(kTaskSupply - 1); + task_semaphore.Release(kTaskSupply - 1); + + // Only one timestamp bound and one packet are emitted. + EXPECT_EQ(outputs.size(), 1); + EXPECT_EQ(thinned_outputs.size(), 1); + + // Allow the second LambdaCalculator::Process call to complete. + exit_semaphore.Release(1); + MP_ASSERT_OK(graph.WaitUntilIdle()); + + // Packets 1 and 2 were dropped by the FixedSizeInputStreamHandler. + EXPECT_EQ(thinned_outputs.size(), 2); + EXPECT_EQ(thinned_outputs[0].Timestamp(), Timestamp(0)); + EXPECT_EQ(thinned_outputs[1].Timestamp(), Timestamp(kNumInputs - 1)); + EXPECT_EQ(outputs.size(), kNumInputs); + MP_ASSERT_OK(graph.CloseAllPacketSources()); + 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 new file mode 100644 index 000000000..166826ff1 --- /dev/null +++ b/mediapipe/framework/calculator_graph_side_packet_test.cc @@ -0,0 +1,747 @@ +// 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 "absl/time/clock.h" +#include "absl/time/time.h" +#include "mediapipe/framework/calculator.pb.h" +#include "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/port/canonical_errors.h" +#include "mediapipe/framework/port/gmock.h" +#include "mediapipe/framework/port/gtest.h" +#include "mediapipe/framework/port/logging.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/status_matchers.h" + +namespace mediapipe { + +namespace { + +// Takes an input stream packet and passes it (with timestamp removed) as an +// output side packet. +class OutputSidePacketInProcessCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->Inputs().Index(0).SetAny(); + cc->OutputSidePackets().Index(0).SetSameAs(&cc->Inputs().Index(0)); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) final { + cc->OutputSidePackets().Index(0).Set( + cc->Inputs().Index(0).Value().At(Timestamp::Unset())); + return ::mediapipe::OkStatus(); + } +}; +REGISTER_CALCULATOR(OutputSidePacketInProcessCalculator); + +// Takes an input stream packet and counts the number of the packets it +// receives. Outputs the total number of packets as a side packet in Close. +class CountAndOutputSummarySidePacketInCloseCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->Inputs().Index(0).SetAny(); + cc->OutputSidePackets().Index(0).Set(); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) final { + ++count_; + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Close(CalculatorContext* cc) final { + cc->OutputSidePackets().Index(0).Set( + MakePacket(count_).At(Timestamp::Unset())); + return ::mediapipe::OkStatus(); + } + + int count_ = 0; +}; +REGISTER_CALCULATOR(CountAndOutputSummarySidePacketInCloseCalculator); + +// Takes an input stream packet and passes it (with timestamp intact) as an +// output side packet. This triggers an error in the graph. +class OutputSidePacketWithTimestampCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->Inputs().Index(0).SetAny(); + cc->OutputSidePackets().Index(0).SetSameAs(&cc->Inputs().Index(0)); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) final { + cc->OutputSidePackets().Index(0).Set(cc->Inputs().Index(0).Value()); + return ::mediapipe::OkStatus(); + } +}; +REGISTER_CALCULATOR(OutputSidePacketWithTimestampCalculator); + +// Generates an output side packet containing the integer 1. +class IntegerOutputSidePacketCalculator : 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(1)); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) final { + LOG(FATAL) << "Not reached."; + return ::mediapipe::OkStatus(); + } +}; +REGISTER_CALCULATOR(IntegerOutputSidePacketCalculator); + +// Generates an output side packet containing the sum of the two integer input +// side packets. +class SidePacketAdderCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->InputSidePackets().Index(0).Set(); + cc->InputSidePackets().Index(1).Set(); + cc->OutputSidePackets().Index(0).Set(); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Open(CalculatorContext* cc) final { + cc->OutputSidePackets().Index(0).Set( + MakePacket(cc->InputSidePackets().Index(1).Get() + + cc->InputSidePackets().Index(0).Get())); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) final { + LOG(FATAL) << "Not reached."; + return ::mediapipe::OkStatus(); + } +}; +REGISTER_CALCULATOR(SidePacketAdderCalculator); + +// Produces an output packet with the PostStream timestamp containing the +// input side packet. +class SidePacketToStreamPacketCalculator : public CalculatorBase { + public: + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->InputSidePackets().Index(0).SetAny(); + cc->Outputs().Index(0).SetSameAs(&cc->InputSidePackets().Index(0)); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Open(CalculatorContext* cc) final { + cc->Outputs().Index(0).AddPacket( + cc->InputSidePackets().Index(0).At(Timestamp::PostStream())); + cc->Outputs().Index(0).Close(); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) final { + return ::mediapipe::tool::StatusStop(); + } +}; +REGISTER_CALCULATOR(SidePacketToStreamPacketCalculator); + +// Packet generator for an arbitrary unit64 packet. +class Uint64PacketGenerator : public PacketGenerator { + public: + static ::mediapipe::Status FillExpectations( + const PacketGeneratorOptions& extendable_options, + PacketTypeSet* input_side_packets, PacketTypeSet* output_side_packets) { + output_side_packets->Index(0).Set(); + return ::mediapipe::OkStatus(); + } + + static ::mediapipe::Status Generate( + const PacketGeneratorOptions& extendable_options, + const PacketSet& input_side_packets, PacketSet* output_side_packets) { + output_side_packets->Index(0) = Adopt(new uint64(15LL << 32 | 5)); + return ::mediapipe::OkStatus(); + } +}; +REGISTER_PACKET_GENERATOR(Uint64PacketGenerator); + +TEST(CalculatorGraph, OutputSidePacketInProcess) { + const int64 offset = 100; + CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie(R"( + input_stream: "offset" + node { + calculator: "OutputSidePacketInProcessCalculator" + input_stream: "offset" + output_side_packet: "offset" + } + node { + calculator: "SidePacketToStreamPacketCalculator" + output_stream: "output" + input_side_packet: "offset" + } + )"); + 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 twice. + for (int run = 0; run < 2; ++run) { + output_packets.clear(); + MP_ASSERT_OK(graph.StartRun({})); + MP_ASSERT_OK(graph.AddPacketToInputStream( + "offset", MakePacket(offset).At(Timestamp(0)))); + MP_ASSERT_OK(graph.CloseInputStream("offset")); + MP_ASSERT_OK(graph.WaitUntilDone()); + ASSERT_EQ(1, output_packets.size()); + EXPECT_EQ(offset, output_packets[0].Get().Value()); + } +} + +// A PacketGenerator that simply passes its input Packets through +// unchanged. The inputs may be specified by tag or index. The outputs +// must match the inputs exactly. Any options may be specified and will +// also be ignored. +class PassThroughGenerator : public PacketGenerator { + public: + static ::mediapipe::Status FillExpectations( + const PacketGeneratorOptions& extendable_options, PacketTypeSet* inputs, + PacketTypeSet* outputs) { + if (!inputs->TagMap()->SameAs(*outputs->TagMap())) { + return ::mediapipe::InvalidArgumentError( + "Input and outputs to PassThroughGenerator must use the same tags " + "and indexes."); + } + for (CollectionItemId id = inputs->BeginId(); id < inputs->EndId(); ++id) { + inputs->Get(id).SetAny(); + outputs->Get(id).SetSameAs(&inputs->Get(id)); + } + return ::mediapipe::OkStatus(); + } + + static ::mediapipe::Status Generate( + const PacketGeneratorOptions& extendable_options, + const PacketSet& input_side_packets, PacketSet* output_side_packets) { + for (CollectionItemId id = input_side_packets.BeginId(); + id < input_side_packets.EndId(); ++id) { + output_side_packets->Get(id) = input_side_packets.Get(id); + } + return ::mediapipe::OkStatus(); + } +}; +REGISTER_PACKET_GENERATOR(PassThroughGenerator); + +TEST(CalculatorGraph, SharePacketGeneratorGraph) { + CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie(R"( + node { + calculator: 'CountingSourceCalculator' + output_stream: 'count1' + input_side_packet: 'MAX_COUNT:max_count1' + } + node { + calculator: 'CountingSourceCalculator' + output_stream: 'count2' + input_side_packet: 'MAX_COUNT:max_count2' + } + node { + calculator: 'CountingSourceCalculator' + output_stream: 'count3' + input_side_packet: 'MAX_COUNT:max_count3' + } + node { + calculator: 'CountingSourceCalculator' + output_stream: 'count4' + input_side_packet: 'MAX_COUNT:max_count4' + } + node { + calculator: 'PassThroughCalculator' + input_side_packet: 'MAX_COUNT:max_count5' + output_side_packet: 'MAX_COUNT:max_count6' + } + node { + calculator: 'CountingSourceCalculator' + output_stream: 'count5' + input_side_packet: 'MAX_COUNT:max_count6' + } + packet_generator { + packet_generator: 'PassThroughGenerator' + input_side_packet: 'max_count1' + output_side_packet: 'max_count2' + } + packet_generator { + packet_generator: 'PassThroughGenerator' + input_side_packet: 'max_count4' + output_side_packet: 'max_count5' + } + )"); + + // At this point config is a standard config which specifies both + // calculators and packet_factories/packet_genators. The following + // code is an example of reusing side packets across a number of + // CalculatorGraphs. It is particularly informative to note how each + // side packet is created. + // + // max_count1 is set for all graphs by a PacketFactory in the config. + // The side packet is created by generator_graph.InitializeGraph(). + // + // max_count2 is set for all graphs by a PacketGenerator in the config. + // The side packet is created by generator_graph.InitializeGraph() + // because max_count1 is available at that time. + // + // max_count3 is set for all graphs by directly being specified as an + // argument to generator_graph.InitializeGraph(). + // + // max_count4 is set per graph because it is directly specified as an + // argument to generator_graph.ProcessGraph(). + // + // max_count5 is set per graph by a PacketGenerator which is run when + // generator_graph.ProcessGraph() is run (because max_count4 isn't + // available until then). + + // Before anything else, split the graph config into two parts, one + // with the PacketFactory and PacketGenerator config and the other + // with the Calculator config. + CalculatorGraphConfig calculator_config = config; + calculator_config.clear_packet_factory(); + calculator_config.clear_packet_generator(); + CalculatorGraphConfig generator_config = config; + generator_config.clear_node(); + + // Next, create a ValidatedGraphConfig for both configs. + ValidatedGraphConfig validated_calculator_config; + MP_ASSERT_OK(validated_calculator_config.Initialize(calculator_config)); + ValidatedGraphConfig validated_generator_config; + MP_ASSERT_OK(validated_generator_config.Initialize(generator_config)); + + // Create a PacketGeneratorGraph. Side packets max_count1, max_count2, + // and max_count3 are created upon initialization. + // Note that validated_generator_config must outlive generator_graph. + PacketGeneratorGraph generator_graph; + MP_ASSERT_OK( + generator_graph.Initialize(&validated_generator_config, nullptr, + {{"max_count1", MakePacket(10)}, + {"max_count3", MakePacket(20)}})); + ASSERT_THAT(generator_graph.BasePackets(), + testing::ElementsAre(testing::Key("max_count1"), + testing::Key("max_count2"), + testing::Key("max_count3"))); + + // Create a bunch of graphs. + std::vector> graphs; + for (int i = 0; i < 100; ++i) { + graphs.emplace_back(absl::make_unique()); + // Do not pass extra side packets here. + // Note that validated_calculator_config must outlive the graph. + MP_ASSERT_OK(graphs.back()->Initialize(calculator_config, {})); + } + // Run a bunch of graphs, reusing side packets max_count1, max_count2, + // and max_count3. The side packet max_count4 is added per run, + // and triggers the execution of a packet generator which generates + // max_count5. + for (int i = 0; i < 100; ++i) { + std::map all_side_packets; + // Creates max_count4 and max_count5. + MP_ASSERT_OK(generator_graph.RunGraphSetup( + {{"max_count4", MakePacket(30 + i)}}, &all_side_packets)); + ASSERT_THAT(all_side_packets, + testing::ElementsAre( + testing::Key("max_count1"), testing::Key("max_count2"), + testing::Key("max_count3"), testing::Key("max_count4"), + testing::Key("max_count5"))); + // Pass all the side packets prepared by generator_graph here. + MP_ASSERT_OK(graphs[i]->Run(all_side_packets)); + // TODO Verify the actual output. + } + + // Destroy all the graphs. + graphs.clear(); +} + +TEST(CalculatorGraph, OutputSidePacketAlreadySet) { + const int64 offset = 100; + CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie(R"( + input_stream: "offset" + node { + calculator: "OutputSidePacketInProcessCalculator" + input_stream: "offset" + output_side_packet: "offset" + } + )"); + CalculatorGraph graph; + MP_ASSERT_OK(graph.Initialize(config)); + MP_ASSERT_OK(graph.StartRun({})); + // Send two input packets to cause OutputSidePacketInProcessCalculator to + // set the output side packet twice. + MP_ASSERT_OK(graph.AddPacketToInputStream( + "offset", MakePacket(offset).At(Timestamp(0)))); + MP_ASSERT_OK(graph.AddPacketToInputStream( + "offset", MakePacket(offset).At(Timestamp(1)))); + MP_ASSERT_OK(graph.CloseInputStream("offset")); + + ::mediapipe::Status status = graph.WaitUntilDone(); + EXPECT_EQ(status.code(), ::mediapipe::StatusCode::kAlreadyExists); + EXPECT_THAT(status.message(), testing::HasSubstr("was already set.")); +} + +TEST(CalculatorGraph, OutputSidePacketWithTimestamp) { + const int64 offset = 100; + CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie(R"( + input_stream: "offset" + node { + calculator: "OutputSidePacketWithTimestampCalculator" + input_stream: "offset" + output_side_packet: "offset" + } + )"); + CalculatorGraph graph; + MP_ASSERT_OK(graph.Initialize(config)); + MP_ASSERT_OK(graph.StartRun({})); + // The OutputSidePacketWithTimestampCalculator neglects to clear the + // timestamp in the input packet when it copies the input packet to the + // output side packet. The timestamp value should appear in the error + // message. + MP_ASSERT_OK(graph.AddPacketToInputStream( + "offset", MakePacket(offset).At(Timestamp(237)))); + MP_ASSERT_OK(graph.CloseInputStream("offset")); + ::mediapipe::Status status = graph.WaitUntilDone(); + EXPECT_EQ(status.code(), ::mediapipe::StatusCode::kInvalidArgument); + EXPECT_THAT(status.message(), testing::HasSubstr("has a timestamp 237.")); +} + +TEST(CalculatorGraph, OutputSidePacketConsumedBySourceNode) { + const int max_count = 10; + CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie(R"( + input_stream: "max_count" + node { + calculator: "OutputSidePacketInProcessCalculator" + input_stream: "max_count" + output_side_packet: "max_count" + } + node { + calculator: "CountingSourceCalculator" + output_stream: "count" + input_side_packet: "MAX_COUNT:max_count" + } + node { + calculator: "PassThroughCalculator" + input_stream: "count" + 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(); + })); + MP_ASSERT_OK(graph.StartRun({})); + // Wait until the graph is idle so that + // Scheduler::TryToScheduleNextSourceLayer() gets called. + // Scheduler::TryToScheduleNextSourceLayer() should not activate source + // nodes that haven't been opened. We can't call graph.WaitUntilIdle() + // because the graph has a source node. + absl::SleepFor(absl::Milliseconds(10)); + MP_ASSERT_OK(graph.AddPacketToInputStream( + "max_count", MakePacket(max_count).At(Timestamp(0)))); + MP_ASSERT_OK(graph.CloseInputStream("max_count")); + MP_ASSERT_OK(graph.WaitUntilDone()); + ASSERT_EQ(max_count, output_packets.size()); + for (int i = 0; i < output_packets.size(); ++i) { + EXPECT_EQ(i, output_packets[i].Get()); + EXPECT_EQ(Timestamp(i), output_packets[i].Timestamp()); + } +} + +// Returns the first packet of the input stream. +class FirstPacketFilterCalculator : public CalculatorBase { + public: + FirstPacketFilterCalculator() {} + ~FirstPacketFilterCalculator() override {} + + static ::mediapipe::Status GetContract(CalculatorContract* cc) { + cc->Inputs().Index(0).SetAny(); + cc->Outputs().Index(0).SetSameAs(&cc->Inputs().Index(0)); + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Process(CalculatorContext* cc) override { + if (!seen_first_packet_) { + cc->Outputs().Index(0).AddPacket(cc->Inputs().Index(0).Value()); + cc->Outputs().Index(0).Close(); + seen_first_packet_ = true; + } + return ::mediapipe::OkStatus(); + } + + private: + bool seen_first_packet_ = false; +}; +REGISTER_CALCULATOR(FirstPacketFilterCalculator); + +TEST(CalculatorGraph, SourceLayerInversion) { + // There are three CountingSourceCalculators, indexed 0, 1, and 2. Each of + // them outputs 10 packets. + // + // CountingSourceCalculator 0 should output 0, 1, 2, 3, ..., 9. + // CountingSourceCalculator 1 should output 100, 101, 102, 103, ..., 109. + // CountingSourceCalculator 2 should output 0, 100, 200, 300, ..., 900. + // However, there is a source layer inversion. + // CountingSourceCalculator 0 is in source layer 0. + // CountingSourceCalculator 1 is in source layer 1. + // CountingSourceCalculator 2 is in source layer 0, but consumes an output + // side packet generated by a downstream calculator of + // CountingSourceCalculator 1. + // + // This graph will deadlock when CountingSourceCalculator 0 runs to + // completion and CountingSourceCalculator 1 cannot be activated because + // CountingSourceCalculator 2 cannot be opened. + + const int max_count = 10; + const int initial_value1 = 100; + // Set num_threads to 1 to force sequential execution for deterministic + // outputs. + CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie(R"( + num_threads: 1 + node { + calculator: "CountingSourceCalculator" + output_stream: "count0" + input_side_packet: "MAX_COUNT:max_count" + source_layer: 0 + } + + node { + calculator: "CountingSourceCalculator" + output_stream: "count1" + input_side_packet: "MAX_COUNT:max_count" + input_side_packet: "INITIAL_VALUE:initial_value1" + source_layer: 1 + } + node { + calculator: "FirstPacketFilterCalculator" + input_stream: "count1" + output_stream: "first_count1" + } + node { + calculator: "OutputSidePacketInProcessCalculator" + input_stream: "first_count1" + output_side_packet: "increment2" + } + + node { + calculator: "CountingSourceCalculator" + output_stream: "count2" + input_side_packet: "MAX_COUNT:max_count" + input_side_packet: "INCREMENT:increment2" + source_layer: 0 + } + )"); + CalculatorGraph graph; + MP_ASSERT_OK(graph.Initialize( + config, {{"max_count", MakePacket(max_count)}, + {"initial_value1", MakePacket(initial_value1)}})); + ::mediapipe::Status status = graph.Run(); + EXPECT_EQ(status.code(), ::mediapipe::StatusCode::kUnknown); + EXPECT_THAT(status.message(), testing::HasSubstr("deadlock")); +} + +// Tests a graph of packet-generator-like calculators, which have no input +// streams and no output streams. +TEST(CalculatorGraph, PacketGeneratorLikeCalculators) { + CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie(R"( + node { + calculator: "IntegerOutputSidePacketCalculator" + output_side_packet: "one" + } + node { + calculator: "IntegerOutputSidePacketCalculator" + output_side_packet: "another_one" + } + node { + calculator: "SidePacketAdderCalculator" + input_side_packet: "one" + input_side_packet: "another_one" + output_side_packet: "two" + } + node { + calculator: "IntegerOutputSidePacketCalculator" + output_side_packet: "yet_another_one" + } + node { + calculator: "SidePacketAdderCalculator" + input_side_packet: "two" + input_side_packet: "yet_another_one" + output_side_packet: "three" + } + node { + calculator: "SidePacketToStreamPacketCalculator" + input_side_packet: "three" + 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(); + })); + MP_ASSERT_OK(graph.Run()); + ASSERT_EQ(1, output_packets.size()); + EXPECT_EQ(3, output_packets[0].Get()); + EXPECT_EQ(Timestamp::PostStream(), output_packets[0].Timestamp()); +} + +TEST(CalculatorGraph, OutputSummarySidePacketInClose) { + CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie(R"( + input_stream: "input_packets" + node { + calculator: "CountAndOutputSummarySidePacketInCloseCalculator" + input_stream: "input_packets" + output_side_packet: "num_of_packets" + } + node { + calculator: "SidePacketToStreamPacketCalculator" + input_side_packet: "num_of_packets" + 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 twice. + int max_count = 100; + for (int run = 0; run < 1; ++run) { + output_packets.clear(); + MP_ASSERT_OK(graph.StartRun({})); + for (int i = 0; i < max_count; ++i) { + MP_ASSERT_OK(graph.AddPacketToInputStream( + "input_packets", MakePacket(i).At(Timestamp(i)))); + } + MP_ASSERT_OK(graph.CloseInputStream("input_packets")); + MP_ASSERT_OK(graph.WaitUntilDone()); + ASSERT_EQ(1, output_packets.size()); + EXPECT_EQ(max_count, output_packets[0].Get()); + EXPECT_EQ(Timestamp::PostStream(), output_packets[0].Timestamp()); + } +} + +TEST(CalculatorGraph, GetOutputSidePacket) { + CalculatorGraphConfig config = + ::mediapipe::ParseTextProtoOrDie(R"( + input_stream: "input_packets" + node { + calculator: "CountAndOutputSummarySidePacketInCloseCalculator" + input_stream: "input_packets" + output_side_packet: "num_of_packets" + } + packet_generator { + packet_generator: "Uint64PacketGenerator" + output_side_packet: "output_uint64" + } + packet_generator { + packet_generator: "IntSplitterPacketGenerator" + input_side_packet: "input_uint64" + output_side_packet: "output_uint32_pair" + } + )"); + CalculatorGraph graph; + MP_ASSERT_OK(graph.Initialize(config)); + // Check a packet generated by the PacketGenerator, which is available after + // graph initialization, can be fetched before graph starts. + ::mediapipe::StatusOr status_or_packet = + graph.GetOutputSidePacket("output_uint64"); + MP_ASSERT_OK(status_or_packet); + EXPECT_EQ(Timestamp::Unset(), status_or_packet.ValueOrDie().Timestamp()); + // IntSplitterPacketGenerator is missing its input side packet and we + // won't be able to get its output side packet now. + status_or_packet = graph.GetOutputSidePacket("output_uint32_pair"); + EXPECT_EQ(::mediapipe::StatusCode::kUnavailable, + status_or_packet.status().code()); + // Run the graph twice. + int max_count = 100; + std::map extra_side_packets; + extra_side_packets.insert({"input_uint64", MakePacket(1123)}); + for (int run = 0; run < 1; ++run) { + MP_ASSERT_OK(graph.StartRun(extra_side_packets)); + status_or_packet = graph.GetOutputSidePacket("output_uint32_pair"); + MP_ASSERT_OK(status_or_packet); + EXPECT_EQ(Timestamp::Unset(), status_or_packet.ValueOrDie().Timestamp()); + for (int i = 0; i < max_count; ++i) { + MP_ASSERT_OK(graph.AddPacketToInputStream( + "input_packets", MakePacket(i).At(Timestamp(i)))); + } + MP_ASSERT_OK(graph.CloseInputStream("input_packets")); + + // Should return NOT_FOUND for invalid side packets. + status_or_packet = graph.GetOutputSidePacket("unknown"); + EXPECT_FALSE(status_or_packet.ok()); + EXPECT_EQ(::mediapipe::StatusCode::kNotFound, + status_or_packet.status().code()); + // Should return UNAVAILABLE before graph is done for valid non-base + // packets. + status_or_packet = graph.GetOutputSidePacket("num_of_packets"); + EXPECT_FALSE(status_or_packet.ok()); + EXPECT_EQ(::mediapipe::StatusCode::kUnavailable, + status_or_packet.status().code()); + // Should stil return a base even before graph is done. + status_or_packet = graph.GetOutputSidePacket("output_uint64"); + MP_ASSERT_OK(status_or_packet); + EXPECT_EQ(Timestamp::Unset(), status_or_packet.ValueOrDie().Timestamp()); + + MP_ASSERT_OK(graph.WaitUntilDone()); + + // Check packets are available after graph is done. + status_or_packet = graph.GetOutputSidePacket("num_of_packets"); + MP_ASSERT_OK(status_or_packet); + EXPECT_EQ(max_count, status_or_packet.ValueOrDie().Get()); + EXPECT_EQ(Timestamp::Unset(), status_or_packet.ValueOrDie().Timestamp()); + // Should still return a base packet after graph is done. + status_or_packet = graph.GetOutputSidePacket("output_uint64"); + MP_ASSERT_OK(status_or_packet); + EXPECT_EQ(Timestamp::Unset(), status_or_packet.ValueOrDie().Timestamp()); + // Should still return a non-base packet after graph is done. + status_or_packet = graph.GetOutputSidePacket("output_uint32_pair"); + MP_ASSERT_OK(status_or_packet); + EXPECT_EQ(Timestamp::Unset(), status_or_packet.ValueOrDie().Timestamp()); + } +} + +} // namespace +} // namespace mediapipe diff --git a/mediapipe/framework/calculator_graph_test.cc b/mediapipe/framework/calculator_graph_test.cc index 3f9505250..f45fd5175 100644 --- a/mediapipe/framework/calculator_graph_test.cc +++ b/mediapipe/framework/calculator_graph_test.cc @@ -32,6 +32,7 @@ #include "absl/strings/string_view.h" #include "absl/strings/substitute.h" #include "absl/time/clock.h" +#include "absl/time/time.h" #include "mediapipe/framework/calculator_framework.h" #include "mediapipe/framework/collection_item_id.h" #include "mediapipe/framework/counter_factory.h" @@ -355,97 +356,6 @@ class IntToFloatCalculator : public CalculatorBase { }; REGISTER_CALCULATOR(IntToFloatCalculator); -// A Calculator that passes an input packet through if it contains an even -// integer. -class EvenIntFilterCalculator : public CalculatorBase { - public: - static ::mediapipe::Status GetContract(CalculatorContract* cc) { - cc->Inputs().Index(0).Set(); - cc->Outputs().Index(0).Set(); - return ::mediapipe::OkStatus(); - } - - ::mediapipe::Status Process(CalculatorContext* cc) final { - int value = cc->Inputs().Index(0).Get(); - if (value % 2 == 0) { - cc->Outputs().Index(0).AddPacket(cc->Inputs().Index(0).Value()); - } else { - cc->Outputs().Index(0).SetNextTimestampBound( - cc->InputTimestamp().NextAllowedInStream()); - } - return ::mediapipe::OkStatus(); - } -}; -REGISTER_CALCULATOR(EvenIntFilterCalculator); - -// A Calculator that passes packets through or not, depending on a second -// input. The first input stream's packets are only propagated if the second -// input stream carries the value true. -class ValveCalculator : public CalculatorBase { - public: - static ::mediapipe::Status GetContract(CalculatorContract* cc) { - cc->Inputs().Index(0).SetAny(); - cc->Inputs().Index(1).Set(); - cc->Outputs().Index(0).SetSameAs(&cc->Inputs().Index(0)); - return ::mediapipe::OkStatus(); - } - - ::mediapipe::Status Open(CalculatorContext* cc) final { - cc->Outputs().Index(0).SetHeader(cc->Inputs().Index(0).Header()); - return ::mediapipe::OkStatus(); - } - - ::mediapipe::Status Process(CalculatorContext* cc) final { - if (cc->Inputs().Index(1).Get()) { - cc->GetCounter("PassThrough")->Increment(); - cc->Outputs().Index(0).AddPacket(cc->Inputs().Index(0).Value()); - } else { - cc->GetCounter("Block")->Increment(); - // The next timestamp bound is the minimum timestamp that the next packet - // can have, so, if we want to inform the downstream that no packet at - // InputTimestamp() is coming, we need to set it to the next value. - // We could also just call SetOffset(TimestampDiff(0)) in Open, and then - // we would not have to call this manually. - cc->Outputs().Index(0).SetNextTimestampBound( - cc->InputTimestamp().NextAllowedInStream()); - } - return ::mediapipe::OkStatus(); - } -}; -REGISTER_CALCULATOR(ValveCalculator); - -// A Calculator that simply passes its input Packets and header through, -// but shifts the timestamp. -class TimeShiftCalculator : public CalculatorBase { - public: - static ::mediapipe::Status GetContract(CalculatorContract* cc) { - cc->Inputs().Index(0).SetAny(); - cc->Outputs().Index(0).SetSameAs(&cc->Inputs().Index(0)); - cc->InputSidePackets().Index(0).Set(); - return ::mediapipe::OkStatus(); - } - - ::mediapipe::Status Open(CalculatorContext* cc) final { - // Input: arbitrary Packets. - // Output: copy of the input. - cc->Outputs().Index(0).SetHeader(cc->Inputs().Index(0).Header()); - shift_ = cc->InputSidePackets().Index(0).Get(); - cc->SetOffset(shift_); - return ::mediapipe::OkStatus(); - } - - ::mediapipe::Status Process(CalculatorContext* cc) final { - cc->GetCounter("PassThrough")->Increment(); - cc->Outputs().Index(0).AddPacket( - cc->Inputs().Index(0).Value().At(cc->InputTimestamp() + shift_)); - return ::mediapipe::OkStatus(); - } - - private: - TimestampDiff shift_; -}; -REGISTER_CALCULATOR(TimeShiftCalculator); - template class TypedEmptySourceCalculator : public CalculatorBase { public: @@ -1055,74 +965,6 @@ class OneShot20MsCalculator : public CalculatorBase { }; REGISTER_CALCULATOR(OneShot20MsCalculator); -// A source calculator that alternates between outputting an integer (0, 1, 2, -// ..., 100) and setting the next timestamp bound. The timestamps of the output -// packets and next timestamp bounds are 0, 10, 20, 30, ... -// -// T=0 Output 0 -// T=10 Set timestamp bound -// T=20 Output 1 -// T=30 Set timestamp bound -// ... -// T=2000 Output 100 -class OutputAndBoundSourceCalculator : public CalculatorBase { - public: - static ::mediapipe::Status GetContract(CalculatorContract* cc) { - cc->Outputs().Index(0).Set(); - return ::mediapipe::OkStatus(); - } - - ::mediapipe::Status Open(CalculatorContext* cc) override { - counter_ = 0; - return ::mediapipe::OkStatus(); - } - - ::mediapipe::Status Process(CalculatorContext* cc) override { - Timestamp timestamp(counter_); - if (counter_ % 20 == 0) { - cc->Outputs().Index(0).AddPacket( - MakePacket(counter_ / 20).At(timestamp)); - } else { - cc->Outputs().Index(0).SetNextTimestampBound(timestamp); - } - if (counter_ == 2000) { - return tool::StatusStop(); - } - counter_ += 10; - return ::mediapipe::OkStatus(); - } - - private: - int counter_; -}; -REGISTER_CALCULATOR(OutputAndBoundSourceCalculator); - -// A calculator that outputs an initial packet of value 0 at time 0 in the -// Open() method, and then delays each input packet by 20 time units in the -// Process() method. The input stream and output stream have the integer type. -class Delay20Calculator : 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(20)); - cc->Outputs().Index(0).AddPacket(MakePacket(0).At(Timestamp(0))); - return ::mediapipe::OkStatus(); - } - - ::mediapipe::Status Process(CalculatorContext* cc) final { - const Packet& packet = cc->Inputs().Index(0).Value(); - Timestamp timestamp = packet.Timestamp() + 20; - cc->Outputs().Index(0).AddPacket(packet.At(timestamp)); - return ::mediapipe::OkStatus(); - } -}; -REGISTER_CALCULATOR(Delay20Calculator); - // A source calculator that outputs a packet containing the return value of // pthread_self() (the pthread id of the current thread). class PthreadSelfSourceCalculator : public CalculatorBase { @@ -1322,116 +1164,6 @@ class OutputSidePacketInProcessCalculator : public CalculatorBase { }; REGISTER_CALCULATOR(OutputSidePacketInProcessCalculator); -// Takes an input stream packet and counts the number of the packets it -// receives. Outputs the total number of packets as a side packet in Close. -class CountAndOutputSummarySidePacketInCloseCalculator : public CalculatorBase { - public: - static ::mediapipe::Status GetContract(CalculatorContract* cc) { - cc->Inputs().Index(0).SetAny(); - cc->OutputSidePackets().Index(0).Set(); - return ::mediapipe::OkStatus(); - } - - ::mediapipe::Status Process(CalculatorContext* cc) final { - ++count_; - return ::mediapipe::OkStatus(); - } - - ::mediapipe::Status Close(CalculatorContext* cc) final { - cc->OutputSidePackets().Index(0).Set( - MakePacket(count_).At(Timestamp::Unset())); - return ::mediapipe::OkStatus(); - } - - int count_ = 0; -}; -REGISTER_CALCULATOR(CountAndOutputSummarySidePacketInCloseCalculator); - -// Takes an input stream packet and passes it (with timestamp intact) as an -// output side packet. This triggers an error in the graph. -class OutputSidePacketWithTimestampCalculator : public CalculatorBase { - public: - static ::mediapipe::Status GetContract(CalculatorContract* cc) { - cc->Inputs().Index(0).SetAny(); - cc->OutputSidePackets().Index(0).SetSameAs(&cc->Inputs().Index(0)); - return ::mediapipe::OkStatus(); - } - - ::mediapipe::Status Process(CalculatorContext* cc) final { - cc->OutputSidePackets().Index(0).Set(cc->Inputs().Index(0).Value()); - return ::mediapipe::OkStatus(); - } -}; -REGISTER_CALCULATOR(OutputSidePacketWithTimestampCalculator); - -// Generates an output side packet containing the integer 1. -class IntegerOutputSidePacketCalculator : 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(1)); - return ::mediapipe::OkStatus(); - } - - ::mediapipe::Status Process(CalculatorContext* cc) final { - LOG(FATAL) << "Not reached."; - return ::mediapipe::OkStatus(); - } -}; -REGISTER_CALCULATOR(IntegerOutputSidePacketCalculator); - -// Generates an output side packet containing the sum of the two integer input -// side packets. -class SidePacketAdderCalculator : public CalculatorBase { - public: - static ::mediapipe::Status GetContract(CalculatorContract* cc) { - cc->InputSidePackets().Index(0).Set(); - cc->InputSidePackets().Index(1).Set(); - cc->OutputSidePackets().Index(0).Set(); - return ::mediapipe::OkStatus(); - } - - ::mediapipe::Status Open(CalculatorContext* cc) final { - cc->OutputSidePackets().Index(0).Set( - MakePacket(cc->InputSidePackets().Index(1).Get() + - cc->InputSidePackets().Index(0).Get())); - return ::mediapipe::OkStatus(); - } - - ::mediapipe::Status Process(CalculatorContext* cc) final { - LOG(FATAL) << "Not reached."; - return ::mediapipe::OkStatus(); - } -}; -REGISTER_CALCULATOR(SidePacketAdderCalculator); - -// Produces an output packet with the PostStream timestamp containing the -// input side packet. -class SidePacketToStreamPacketCalculator : public CalculatorBase { - public: - static ::mediapipe::Status GetContract(CalculatorContract* cc) { - cc->InputSidePackets().Index(0).SetAny(); - cc->Outputs().Index(0).SetSameAs(&cc->InputSidePackets().Index(0)); - return ::mediapipe::OkStatus(); - } - - ::mediapipe::Status Open(CalculatorContext* cc) final { - cc->Outputs().Index(0).AddPacket( - cc->InputSidePackets().Index(0).At(Timestamp::PostStream())); - cc->Outputs().Index(0).Close(); - return ::mediapipe::OkStatus(); - } - - ::mediapipe::Status Process(CalculatorContext* cc) final { - return ::mediapipe::tool::StatusStop(); - } -}; -REGISTER_CALCULATOR(SidePacketToStreamPacketCalculator); - // A calculator checks if either of two input streams contains a packet and // sends the packet to the single output stream with the same timestamp. class SimpleMuxCalculator : public CalculatorBase { @@ -2411,205 +2143,6 @@ TEST(CalculatorGraph, InputPacketLifetime) { MP_EXPECT_OK(graph.WaitUntilDone()); } -// Test that SetNextTimestampBound propagates. -TEST(CalculatorGraph, SetNextTimestampBoundPropagation) { - CalculatorGraph graph; - CalculatorGraphConfig config = - ::mediapipe::ParseTextProtoOrDie(R"( - input_stream: 'in' - input_stream: 'gate' - node { - calculator: 'ValveCalculator' - input_stream: 'in' - input_stream: 'gate' - output_stream: 'gated' - } - node { - calculator: 'PassThroughCalculator' - input_stream: 'gated' - output_stream: 'passed' - } - node { - calculator: 'TimeShiftCalculator' - input_stream: 'passed' - output_stream: 'shifted' - input_side_packet: 'shift' - } - node { - calculator: 'MergeCalculator' - input_stream: 'in' - input_stream: 'shifted' - output_stream: 'merged' - } - node { - name: 'merged_output' - calculator: 'PassThroughCalculator' - input_stream: 'merged' - output_stream: 'out' - } - )"); - - Timestamp timestamp = Timestamp(0); - auto send_inputs = [&graph, ×tamp](int input, bool pass) { - ++timestamp; - MP_EXPECT_OK(graph.AddPacketToInputStream( - "in", MakePacket(input).At(timestamp))); - MP_EXPECT_OK(graph.AddPacketToInputStream( - "gate", MakePacket(pass).At(timestamp))); - }; - - MP_ASSERT_OK(graph.Initialize(config)); - MP_ASSERT_OK(graph.StartRun({{"shift", MakePacket(0)}})); - - auto pass_counter = - graph.GetCounterFactory()->GetCounter("ValveCalculator-PassThrough"); - auto block_counter = - graph.GetCounterFactory()->GetCounter("ValveCalculator-Block"); - auto merged_counter = - graph.GetCounterFactory()->GetCounter("merged_output-PassThrough"); - - send_inputs(1, true); - send_inputs(2, true); - send_inputs(3, false); - send_inputs(4, false); - MP_ASSERT_OK(graph.WaitUntilIdle()); - - // Verify that MergeCalculator was able to run even when the gated branch - // was blocked. - EXPECT_EQ(2, pass_counter->Get()); - EXPECT_EQ(2, block_counter->Get()); - EXPECT_EQ(4, merged_counter->Get()); - - send_inputs(5, true); - send_inputs(6, false); - MP_ASSERT_OK(graph.WaitUntilIdle()); - - EXPECT_EQ(3, pass_counter->Get()); - EXPECT_EQ(3, block_counter->Get()); - EXPECT_EQ(6, merged_counter->Get()); - - MP_ASSERT_OK(graph.CloseAllInputStreams()); - MP_ASSERT_OK(graph.WaitUntilDone()); - - // Now test with time shift - MP_ASSERT_OK(graph.StartRun({{"shift", MakePacket(-1)}})); - - send_inputs(7, true); - MP_ASSERT_OK(graph.WaitUntilIdle()); - - // The merger should have run only once now, at timestamp 6, with inputs - // . If we do not respect the offset and unblock the merger for - // timestamp 7 too, then it will have run twice, with 6: and - // 7: <7, null>. - EXPECT_EQ(4, pass_counter->Get()); - EXPECT_EQ(3, block_counter->Get()); - EXPECT_EQ(7, merged_counter->Get()); - - MP_ASSERT_OK(graph.CloseAllInputStreams()); - MP_ASSERT_OK(graph.WaitUntilDone()); - - EXPECT_EQ(4, pass_counter->Get()); - EXPECT_EQ(3, block_counter->Get()); - EXPECT_EQ(8, merged_counter->Get()); -} - -// Both input streams of the calculator node have the same next timestamp -// bound. One input stream has a packet at that timestamp. The other input -// stream is empty. We should not run the Process() method of the node in this -// case. -TEST(CalculatorGraph, NotAllInputPacketsAtNextTimestampBoundAvailable) { - // - // in0_unfiltered in1_to_be_filtered - // | | - // | V - // | +-----------------------+ - // | |EvenIntFilterCalculator| - // | +-----------------------+ - // | | - // \ / - // \ / in1_filtered - // \ / - // | | - // V V - // +------------------+ - // |IntAdderCalculator| - // +------------------+ - // | - // V - // out - // - CalculatorGraph graph; - CalculatorGraphConfig config = - ::mediapipe::ParseTextProtoOrDie(R"( - input_stream: 'in0_unfiltered' - input_stream: 'in1_to_be_filtered' - node { - calculator: 'EvenIntFilterCalculator' - input_stream: 'in1_to_be_filtered' - output_stream: 'in1_filtered' - } - node { - calculator: 'IntAdderCalculator' - input_stream: 'in0_unfiltered' - input_stream: 'in1_filtered' - output_stream: 'out' - } - )"); - std::vector packet_dump; - tool::AddVectorSink("out", &config, &packet_dump); - - MP_ASSERT_OK(graph.Initialize(config)); - MP_ASSERT_OK(graph.StartRun({})); - - Timestamp timestamp = Timestamp(0); - - // We send an integer with timestamp 1 to the in0_unfiltered input stream of - // the IntAdderCalculator. We then send an even integer with timestamp 1 to - // the EvenIntFilterCalculator. This packet will go through and - // the IntAdderCalculator will run. The next timestamp bounds of both the - // input streams of the IntAdderCalculator will become 2. - - ++timestamp; // Timestamp 1. - MP_EXPECT_OK(graph.AddPacketToInputStream("in0_unfiltered", - MakePacket(1).At(timestamp))); - MP_EXPECT_OK(graph.AddPacketToInputStream("in1_to_be_filtered", - MakePacket(2).At(timestamp))); - MP_ASSERT_OK(graph.WaitUntilIdle()); - ASSERT_EQ(1, packet_dump.size()); - EXPECT_EQ(3, packet_dump[0].Get()); - - // We send an odd integer with timestamp 2 to the EvenIntFilterCalculator. - // This packet will be filtered out and the next timestamp bound of the - // in1_filtered input stream of the IntAdderCalculator will become 3. - - ++timestamp; // Timestamp 2. - MP_EXPECT_OK(graph.AddPacketToInputStream("in1_to_be_filtered", - MakePacket(3).At(timestamp))); - - // We send an integer with timestamp 3 to the in0_unfiltered input stream of - // the IntAdderCalculator. MediaPipe should propagate the next timestamp bound - // across the IntAdderCalculator but should not run its Process() method. - - ++timestamp; // Timestamp 3. - MP_EXPECT_OK(graph.AddPacketToInputStream("in0_unfiltered", - MakePacket(3).At(timestamp))); - MP_ASSERT_OK(graph.WaitUntilIdle()); - ASSERT_EQ(1, packet_dump.size()); - - // We send an even integer with timestamp 3 to the IntAdderCalculator. This - // packet will go through and the IntAdderCalculator will run. - - MP_EXPECT_OK(graph.AddPacketToInputStream("in1_to_be_filtered", - MakePacket(4).At(timestamp))); - MP_ASSERT_OK(graph.WaitUntilIdle()); - ASSERT_EQ(2, packet_dump.size()); - EXPECT_EQ(7, packet_dump[1].Get()); - - MP_ASSERT_OK(graph.CloseAllInputStreams()); - MP_ASSERT_OK(graph.WaitUntilDone()); - EXPECT_EQ(2, packet_dump.size()); -} - // Demonstrate an if-then-else graph. TEST(CalculatorGraph, IfThenElse) { // This graph has an if-then-else structure. The left branch, selected by the @@ -3616,134 +3149,6 @@ class PassThroughGenerator : public PacketGenerator { } }; REGISTER_PACKET_GENERATOR(PassThroughGenerator); - -TEST(CalculatorGraph, SharePacketGeneratorGraph) { - CalculatorGraphConfig config = - ::mediapipe::ParseTextProtoOrDie(R"( - node { - calculator: 'CountingSourceCalculator' - output_stream: 'count1' - input_side_packet: 'MAX_COUNT:max_count1' - } - node { - calculator: 'CountingSourceCalculator' - output_stream: 'count2' - input_side_packet: 'MAX_COUNT:max_count2' - } - node { - calculator: 'CountingSourceCalculator' - output_stream: 'count3' - input_side_packet: 'MAX_COUNT:max_count3' - } - node { - calculator: 'CountingSourceCalculator' - output_stream: 'count4' - input_side_packet: 'MAX_COUNT:max_count4' - } - node { - calculator: 'PassThroughCalculator' - input_side_packet: 'MAX_COUNT:max_count5' - output_side_packet: 'MAX_COUNT:max_count6' - } - node { - calculator: 'CountingSourceCalculator' - output_stream: 'count5' - input_side_packet: 'MAX_COUNT:max_count6' - } - packet_generator { - packet_generator: 'PassThroughGenerator' - input_side_packet: 'max_count1' - output_side_packet: 'max_count2' - } - packet_generator { - packet_generator: 'PassThroughGenerator' - input_side_packet: 'max_count4' - output_side_packet: 'max_count5' - } - )"); - - // At this point config is a standard config which specifies both - // calculators and packet_factories/packet_genators. The following - // code is an example of reusing side packets across a number of - // CalculatorGraphs. It is particularly informative to note how each - // side packet is created. - // - // max_count1 is set for all graphs by a PacketFactory in the config. - // The side packet is created by generator_graph.InitializeGraph(). - // - // max_count2 is set for all graphs by a PacketGenerator in the config. - // The side packet is created by generator_graph.InitializeGraph() - // because max_count1 is available at that time. - // - // max_count3 is set for all graphs by directly being specified as an - // argument to generator_graph.InitializeGraph(). - // - // max_count4 is set per graph because it is directly specified as an - // argument to generator_graph.ProcessGraph(). - // - // max_count5 is set per graph by a PacketGenerator which is run when - // generator_graph.ProcessGraph() is run (because max_count4 isn't - // available until then). - - // Before anything else, split the graph config into two parts, one - // with the PacketFactory and PacketGenerator config and the other - // with the Calculator config. - CalculatorGraphConfig calculator_config = config; - calculator_config.clear_packet_factory(); - calculator_config.clear_packet_generator(); - CalculatorGraphConfig generator_config = config; - generator_config.clear_node(); - - // Next, create a ValidatedGraphConfig for both configs. - ValidatedGraphConfig validated_calculator_config; - MP_ASSERT_OK(validated_calculator_config.Initialize(calculator_config)); - ValidatedGraphConfig validated_generator_config; - MP_ASSERT_OK(validated_generator_config.Initialize(generator_config)); - - // Create a PacketGeneratorGraph. Side packets max_count1, max_count2, - // and max_count3 are created upon initialization. - // Note that validated_generator_config must outlive generator_graph. - PacketGeneratorGraph generator_graph; - MP_ASSERT_OK( - generator_graph.Initialize(&validated_generator_config, nullptr, - {{"max_count1", MakePacket(10)}, - {"max_count3", MakePacket(20)}})); - ASSERT_THAT(generator_graph.BasePackets(), - testing::ElementsAre(testing::Key("max_count1"), - testing::Key("max_count2"), - testing::Key("max_count3"))); - - // Create a bunch of graphs. - std::vector> graphs; - for (int i = 0; i < 100; ++i) { - graphs.emplace_back(absl::make_unique()); - // Do not pass extra side packets here. - // Note that validated_calculator_config must outlive the graph. - MP_ASSERT_OK(graphs.back()->Initialize(calculator_config, {})); - } - // Run a bunch of graphs, reusing side packets max_count1, max_count2, - // and max_count3. The side packet max_count4 is added per run, - // and triggers the execution of a packet generator which generates - // max_count5. - for (int i = 0; i < 100; ++i) { - std::map all_side_packets; - // Creates max_count4 and max_count5. - MP_ASSERT_OK(generator_graph.RunGraphSetup( - {{"max_count4", MakePacket(30 + i)}}, &all_side_packets)); - ASSERT_THAT(all_side_packets, - testing::ElementsAre( - testing::Key("max_count1"), testing::Key("max_count2"), - testing::Key("max_count3"), testing::Key("max_count4"), - testing::Key("max_count5"))); - // Pass all the side packets prepared by generator_graph here. - MP_ASSERT_OK(graphs[i]->Run(all_side_packets)); - // TODO Verify the actual output. - } - - // Destroy all the graphs. - graphs.clear(); -} - TEST(CalculatorGraph, RecoverAfterRunError) { PacketGeneratorGraph generator_graph; CalculatorGraphConfig config = @@ -4371,47 +3776,6 @@ TEST(CalculatorGraph, RecoverAfterPreviousFailInOpen) { } } -TEST(CalculatorGraph, PropagateBoundLoop) { - CalculatorGraphConfig config = - ::mediapipe::ParseTextProtoOrDie(R"( - node { - calculator: 'OutputAndBoundSourceCalculator' - output_stream: 'integers' - } - node { - calculator: 'IntAdderCalculator' - input_stream: 'integers' - input_stream: 'old_sum' - input_stream_info: { - tag_index: ':1' # 'old_sum' - back_edge: true - } - output_stream: 'sum' - input_stream_handler { - input_stream_handler: 'EarlyCloseInputStreamHandler' - } - } - node { - calculator: 'Delay20Calculator' - input_stream: 'sum' - output_stream: 'old_sum' - } - )"); - std::vector packet_dump; - tool::AddVectorSink("sum", &config, &packet_dump); - - CalculatorGraph graph; - MP_ASSERT_OK(graph.Initialize(config)); - MP_ASSERT_OK(graph.Run()); - ASSERT_EQ(101, packet_dump.size()); - int sum = 0; - for (int i = 0; i < 101; ++i) { - sum += i; - EXPECT_EQ(sum, packet_dump[i].Get()); - EXPECT_EQ(Timestamp(i * 20), packet_dump[i].Timestamp()); - } -} - TEST(CalculatorGraph, ReuseValidatedGraphConfig) { CalculatorGraphConfig config = ::mediapipe::ParseTextProtoOrDie(R"( @@ -4979,176 +4343,6 @@ TEST(CalculatorGraph, CheckInputTimestamp2) { MP_ASSERT_OK(graph.Run()); } -TEST(CalculatorGraph, CheckBatchProcessingBoundPropagation) { - // The timestamp bound sent by OutputAndBoundSourceCalculator shouldn't be - // directly propagated to the output stream when PassThroughCalculator has - // anything in its default calculator context for batch processing. Otherwise, - // the sink calculator's input stream should report packet timestamp - // mismatches. - CalculatorGraphConfig config = - ::mediapipe::ParseTextProtoOrDie(R"( - node { - calculator: 'OutputAndBoundSourceCalculator' - output_stream: 'integers' - } - node { - calculator: 'PassThroughCalculator' - input_stream: 'integers' - output_stream: 'output' - input_stream_handler { - input_stream_handler: "DefaultInputStreamHandler" - options: { - [mediapipe.DefaultInputStreamHandlerOptions.ext]: { - batch_size: 10 - } - } - } - } - node { calculator: 'IntSinkCalculator' input_stream: 'output' } - )"); - CalculatorGraph graph; - MP_ASSERT_OK(graph.Initialize(config)); - MP_ASSERT_OK(graph.Run()); -} - -TEST(CalculatorGraph, OutputSidePacketInProcess) { - const int64 offset = 100; - CalculatorGraphConfig config = - ::mediapipe::ParseTextProtoOrDie(R"( - input_stream: "offset" - node { - calculator: "OutputSidePacketInProcessCalculator" - input_stream: "offset" - output_side_packet: "offset" - } - node { - calculator: "SidePacketToStreamPacketCalculator" - output_stream: "output" - input_side_packet: "offset" - } - )"); - 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 twice. - for (int run = 0; run < 2; ++run) { - output_packets.clear(); - MP_ASSERT_OK(graph.StartRun({})); - MP_ASSERT_OK(graph.AddPacketToInputStream( - "offset", MakePacket(offset).At(Timestamp(0)))); - MP_ASSERT_OK(graph.CloseInputStream("offset")); - MP_ASSERT_OK(graph.WaitUntilDone()); - ASSERT_EQ(1, output_packets.size()); - EXPECT_EQ(offset, output_packets[0].Get().Value()); - } -} - -TEST(CalculatorGraph, OutputSidePacketAlreadySet) { - const int64 offset = 100; - CalculatorGraphConfig config = - ::mediapipe::ParseTextProtoOrDie(R"( - input_stream: "offset" - node { - calculator: "OutputSidePacketInProcessCalculator" - input_stream: "offset" - output_side_packet: "offset" - } - )"); - CalculatorGraph graph; - MP_ASSERT_OK(graph.Initialize(config)); - MP_ASSERT_OK(graph.StartRun({})); - // Send two input packets to cause OutputSidePacketInProcessCalculator to - // set the output side packet twice. - MP_ASSERT_OK(graph.AddPacketToInputStream( - "offset", MakePacket(offset).At(Timestamp(0)))); - MP_ASSERT_OK(graph.AddPacketToInputStream( - "offset", MakePacket(offset).At(Timestamp(1)))); - MP_ASSERT_OK(graph.CloseInputStream("offset")); - - ::mediapipe::Status status = graph.WaitUntilDone(); - EXPECT_EQ(status.code(), ::mediapipe::StatusCode::kAlreadyExists); - EXPECT_THAT(status.message(), testing::HasSubstr("was already set.")); -} - -TEST(CalculatorGraph, OutputSidePacketWithTimestamp) { - const int64 offset = 100; - CalculatorGraphConfig config = - ::mediapipe::ParseTextProtoOrDie(R"( - input_stream: "offset" - node { - calculator: "OutputSidePacketWithTimestampCalculator" - input_stream: "offset" - output_side_packet: "offset" - } - )"); - CalculatorGraph graph; - MP_ASSERT_OK(graph.Initialize(config)); - MP_ASSERT_OK(graph.StartRun({})); - // The OutputSidePacketWithTimestampCalculator neglects to clear the - // timestamp in the input packet when it copies the input packet to the - // output side packet. The timestamp value should appear in the error - // message. - MP_ASSERT_OK(graph.AddPacketToInputStream( - "offset", MakePacket(offset).At(Timestamp(237)))); - MP_ASSERT_OK(graph.CloseInputStream("offset")); - ::mediapipe::Status status = graph.WaitUntilDone(); - EXPECT_EQ(status.code(), ::mediapipe::StatusCode::kInvalidArgument); - EXPECT_THAT(status.message(), testing::HasSubstr("has a timestamp 237.")); -} - -TEST(CalculatorGraph, OutputSidePacketConsumedBySourceNode) { - const int max_count = 10; - CalculatorGraphConfig config = - ::mediapipe::ParseTextProtoOrDie(R"( - input_stream: "max_count" - node { - calculator: "OutputSidePacketInProcessCalculator" - input_stream: "max_count" - output_side_packet: "max_count" - } - node { - calculator: "CountingSourceCalculator" - output_stream: "count" - input_side_packet: "MAX_COUNT:max_count" - } - node { - calculator: "PassThroughCalculator" - input_stream: "count" - 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(); - })); - MP_ASSERT_OK(graph.StartRun({})); - // Wait until the graph is idle so that - // Scheduler::TryToScheduleNextSourceLayer() gets called. - // Scheduler::TryToScheduleNextSourceLayer() should not activate source - // nodes that haven't been opened. We can't call graph.WaitUntilIdle() - // because the graph has a source node. - absl::SleepFor(absl::Milliseconds(10)); - MP_ASSERT_OK(graph.AddPacketToInputStream( - "max_count", MakePacket(max_count).At(Timestamp(0)))); - MP_ASSERT_OK(graph.CloseInputStream("max_count")); - MP_ASSERT_OK(graph.WaitUntilDone()); - ASSERT_EQ(max_count, output_packets.size()); - for (int i = 0; i < output_packets.size(); ++i) { - EXPECT_EQ(i, output_packets[i].Get()); - EXPECT_EQ(Timestamp(i), output_packets[i].Timestamp()); - } -} - TEST(CalculatorGraph, GraphInputStreamWithTag) { CalculatorGraphConfig config = ::mediapipe::ParseTextProtoOrDie(R"( @@ -5201,245 +4395,6 @@ class FirstPacketFilterCalculator : public CalculatorBase { bool seen_first_packet_ = false; }; REGISTER_CALCULATOR(FirstPacketFilterCalculator); - -TEST(CalculatorGraph, SourceLayerInversion) { - // There are three CountingSourceCalculators, indexed 0, 1, and 2. Each of - // them outputs 10 packets. - // - // CountingSourceCalculator 0 should output 0, 1, 2, 3, ..., 9. - // CountingSourceCalculator 1 should output 100, 101, 102, 103, ..., 109. - // CountingSourceCalculator 2 should output 0, 100, 200, 300, ..., 900. - // However, there is a source layer inversion. - // CountingSourceCalculator 0 is in source layer 0. - // CountingSourceCalculator 1 is in source layer 1. - // CountingSourceCalculator 2 is in source layer 0, but consumes an output - // side packet generated by a downstream calculator of - // CountingSourceCalculator 1. - // - // This graph will deadlock when CountingSourceCalculator 0 runs to - // completion and CountingSourceCalculator 1 cannot be activated because - // CountingSourceCalculator 2 cannot be opened. - - const int max_count = 10; - const int initial_value1 = 100; - // Set num_threads to 1 to force sequential execution for deterministic - // outputs. - CalculatorGraphConfig config = - ::mediapipe::ParseTextProtoOrDie(R"( - num_threads: 1 - node { - calculator: "CountingSourceCalculator" - output_stream: "count0" - input_side_packet: "MAX_COUNT:max_count" - source_layer: 0 - } - - node { - calculator: "CountingSourceCalculator" - output_stream: "count1" - input_side_packet: "MAX_COUNT:max_count" - input_side_packet: "INITIAL_VALUE:initial_value1" - source_layer: 1 - } - node { - calculator: "FirstPacketFilterCalculator" - input_stream: "count1" - output_stream: "first_count1" - } - node { - calculator: "OutputSidePacketInProcessCalculator" - input_stream: "first_count1" - output_side_packet: "increment2" - } - - node { - calculator: "CountingSourceCalculator" - output_stream: "count2" - input_side_packet: "MAX_COUNT:max_count" - input_side_packet: "INCREMENT:increment2" - source_layer: 0 - } - )"); - CalculatorGraph graph; - MP_ASSERT_OK(graph.Initialize( - config, {{"max_count", MakePacket(max_count)}, - {"initial_value1", MakePacket(initial_value1)}})); - ::mediapipe::Status status = graph.Run(); - EXPECT_EQ(status.code(), ::mediapipe::StatusCode::kUnknown); - EXPECT_THAT(status.message(), testing::HasSubstr("deadlock")); -} - -// Tests a graph of packet-generator-like calculators, which have no input -// streams and no output streams. -TEST(CalculatorGraph, PacketGeneratorLikeCalculators) { - CalculatorGraphConfig config = - ::mediapipe::ParseTextProtoOrDie(R"( - node { - calculator: "IntegerOutputSidePacketCalculator" - output_side_packet: "one" - } - node { - calculator: "IntegerOutputSidePacketCalculator" - output_side_packet: "another_one" - } - node { - calculator: "SidePacketAdderCalculator" - input_side_packet: "one" - input_side_packet: "another_one" - output_side_packet: "two" - } - node { - calculator: "IntegerOutputSidePacketCalculator" - output_side_packet: "yet_another_one" - } - node { - calculator: "SidePacketAdderCalculator" - input_side_packet: "two" - input_side_packet: "yet_another_one" - output_side_packet: "three" - } - node { - calculator: "SidePacketToStreamPacketCalculator" - input_side_packet: "three" - 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(); - })); - MP_ASSERT_OK(graph.Run()); - ASSERT_EQ(1, output_packets.size()); - EXPECT_EQ(3, output_packets[0].Get()); - EXPECT_EQ(Timestamp::PostStream(), output_packets[0].Timestamp()); -} - -TEST(CalculatorGraph, OutputSummarySidePacketInClose) { - CalculatorGraphConfig config = - ::mediapipe::ParseTextProtoOrDie(R"( - input_stream: "input_packets" - node { - calculator: "CountAndOutputSummarySidePacketInCloseCalculator" - input_stream: "input_packets" - output_side_packet: "num_of_packets" - } - node { - calculator: "SidePacketToStreamPacketCalculator" - input_side_packet: "num_of_packets" - 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 twice. - int max_count = 100; - for (int run = 0; run < 1; ++run) { - output_packets.clear(); - MP_ASSERT_OK(graph.StartRun({})); - for (int i = 0; i < max_count; ++i) { - MP_ASSERT_OK(graph.AddPacketToInputStream( - "input_packets", MakePacket(i).At(Timestamp(i)))); - } - MP_ASSERT_OK(graph.CloseInputStream("input_packets")); - MP_ASSERT_OK(graph.WaitUntilDone()); - ASSERT_EQ(1, output_packets.size()); - EXPECT_EQ(max_count, output_packets[0].Get()); - EXPECT_EQ(Timestamp::PostStream(), output_packets[0].Timestamp()); - } -} - -TEST(CalculatorGraph, GetOutputSidePacket) { - CalculatorGraphConfig config = - ::mediapipe::ParseTextProtoOrDie(R"( - input_stream: "input_packets" - node { - calculator: "CountAndOutputSummarySidePacketInCloseCalculator" - input_stream: "input_packets" - output_side_packet: "num_of_packets" - } - packet_generator { - packet_generator: "Uint64PacketGenerator" - output_side_packet: "output_uint64" - } - packet_generator { - packet_generator: "IntSplitterPacketGenerator" - input_side_packet: "input_uint64" - output_side_packet: "output_uint32_pair" - } - )"); - CalculatorGraph graph; - MP_ASSERT_OK(graph.Initialize(config)); - // Check a packet generated by the PacketGenerator, which is available after - // graph initialization, can be fetched before graph starts. - ::mediapipe::StatusOr status_or_packet = - graph.GetOutputSidePacket("output_uint64"); - MP_ASSERT_OK(status_or_packet); - EXPECT_EQ(Timestamp::Unset(), status_or_packet.ValueOrDie().Timestamp()); - // IntSplitterPacketGenerator is missing its input side packet and we - // won't be able to get its output side packet now. - status_or_packet = graph.GetOutputSidePacket("output_uint32_pair"); - EXPECT_EQ(::mediapipe::StatusCode::kUnavailable, - status_or_packet.status().code()); - // Run the graph twice. - int max_count = 100; - std::map extra_side_packets; - extra_side_packets.insert({"input_uint64", MakePacket(1123)}); - for (int run = 0; run < 1; ++run) { - MP_ASSERT_OK(graph.StartRun(extra_side_packets)); - status_or_packet = graph.GetOutputSidePacket("output_uint32_pair"); - MP_ASSERT_OK(status_or_packet); - EXPECT_EQ(Timestamp::Unset(), status_or_packet.ValueOrDie().Timestamp()); - for (int i = 0; i < max_count; ++i) { - MP_ASSERT_OK(graph.AddPacketToInputStream( - "input_packets", MakePacket(i).At(Timestamp(i)))); - } - MP_ASSERT_OK(graph.CloseInputStream("input_packets")); - - // Should return NOT_FOUND for invalid side packets. - status_or_packet = graph.GetOutputSidePacket("unknown"); - EXPECT_FALSE(status_or_packet.ok()); - EXPECT_EQ(::mediapipe::StatusCode::kNotFound, - status_or_packet.status().code()); - // Should return UNAVAILABLE before graph is done for valid non-base - // packets. - status_or_packet = graph.GetOutputSidePacket("num_of_packets"); - EXPECT_FALSE(status_or_packet.ok()); - EXPECT_EQ(::mediapipe::StatusCode::kUnavailable, - status_or_packet.status().code()); - // Should stil return a base even before graph is done. - status_or_packet = graph.GetOutputSidePacket("output_uint64"); - MP_ASSERT_OK(status_or_packet); - EXPECT_EQ(Timestamp::Unset(), status_or_packet.ValueOrDie().Timestamp()); - - MP_ASSERT_OK(graph.WaitUntilDone()); - - // Check packets are available after graph is done. - status_or_packet = graph.GetOutputSidePacket("num_of_packets"); - MP_ASSERT_OK(status_or_packet); - EXPECT_EQ(max_count, status_or_packet.ValueOrDie().Get()); - EXPECT_EQ(Timestamp::Unset(), status_or_packet.ValueOrDie().Timestamp()); - // Should still return a base packet after graph is done. - status_or_packet = graph.GetOutputSidePacket("output_uint64"); - MP_ASSERT_OK(status_or_packet); - EXPECT_EQ(Timestamp::Unset(), status_or_packet.ValueOrDie().Timestamp()); - // Should still return a non-base packet after graph is done. - status_or_packet = graph.GetOutputSidePacket("output_uint32_pair"); - MP_ASSERT_OK(status_or_packet); - EXPECT_EQ(Timestamp::Unset(), status_or_packet.ValueOrDie().Timestamp()); - } -} - constexpr int kDefaultMaxCount = 1000; TEST(CalculatorGraph, TestPollPacket) { diff --git a/mediapipe/framework/formats/matrix_data.proto b/mediapipe/framework/formats/matrix_data.proto index deec5c8df..216d01288 100644 --- a/mediapipe/framework/formats/matrix_data.proto +++ b/mediapipe/framework/formats/matrix_data.proto @@ -34,7 +34,7 @@ message MatrixData { ROW_MAJOR = 1; } - // Order in which the data are stored. Implicitly defaults to COLUMN_MAJOR, - // which matches the default for mediapipe::Matrix and Eigen::Matrix*. - optional Layout layout = 4; + // Order in which the data are stored. Defaults to COLUMN_MAJOR, which matches + // the default for mediapipe::Matrix and Eigen::Matrix*. + optional Layout layout = 4 [default = COLUMN_MAJOR]; } diff --git a/mediapipe/framework/graph_validation_test.cc b/mediapipe/framework/graph_validation_test.cc index c67bf57df..98492b8d0 100644 --- a/mediapipe/framework/graph_validation_test.cc +++ b/mediapipe/framework/graph_validation_test.cc @@ -154,11 +154,13 @@ TEST(ValidatedGraphConfigTest, InitializeTemplateFromProtos) { } )"); auto options = ParseTextProtoOrDie(R"( - [mediapipe.TemplateSubgraphOptions.ext]: { - dict: { - arg: { - key: "in_name" - value: { str: "stream_9" } + options: { + [mediapipe.TemplateSubgraphOptions.ext]: { + dict: { + arg: { + key: "in_name" + value: { str: "stream_9" } + } } } })"); diff --git a/mediapipe/framework/subgraph.cc b/mediapipe/framework/subgraph.cc index a1479c5bf..8d121e01e 100644 --- a/mediapipe/framework/subgraph.cc +++ b/mediapipe/framework/subgraph.cc @@ -44,8 +44,8 @@ TemplateSubgraph::~TemplateSubgraph() {} ::mediapipe::StatusOr TemplateSubgraph::GetConfig( const Subgraph::SubgraphOptions& options) { - const TemplateDict& arguments = - options.GetExtension(TemplateSubgraphOptions::ext).dict(); + TemplateDict arguments = + Subgraph::GetOptions(options).dict(); tool::TemplateExpander expander; CalculatorGraphConfig config; MP_RETURN_IF_ERROR(expander.ExpandTemplates(arguments, templ_, &config)); diff --git a/mediapipe/framework/subgraph.h b/mediapipe/framework/subgraph.h index abfecbc36..3febde7e9 100644 --- a/mediapipe/framework/subgraph.h +++ b/mediapipe/framework/subgraph.h @@ -24,6 +24,7 @@ #include "mediapipe/framework/port/status.h" #include "mediapipe/framework/port/statusor.h" #include "mediapipe/framework/tool/calculator_graph_template.pb.h" +#include "mediapipe/framework/tool/options_util.h" namespace mediapipe { @@ -32,7 +33,7 @@ namespace mediapipe { // the graph is running. class Subgraph { public: - using SubgraphOptions = CalculatorOptions; + using SubgraphOptions = CalculatorGraphConfig::Node; Subgraph(); virtual ~Subgraph(); // Returns the config to use for one instantiation of the subgraph. The @@ -42,6 +43,12 @@ class Subgraph { // TODO: make this static? virtual ::mediapipe::StatusOr GetConfig( const SubgraphOptions& options) = 0; + + // Returns options of a specific type. + template + static T GetOptions(Subgraph::SubgraphOptions supgraph_options) { + return tool::OptionsMap().Initialize(supgraph_options).Get(); + } }; using SubgraphRegistry = GlobalFactoryRegistry>; diff --git a/mediapipe/framework/test_calculators.cc b/mediapipe/framework/test_calculators.cc index 21d46c8b8..f3d1f0c79 100644 --- a/mediapipe/framework/test_calculators.cc +++ b/mediapipe/framework/test_calculators.cc @@ -548,8 +548,12 @@ typedef std::function<::mediapipe::Status(const InputStreamShardSet&, OutputStreamShardSet*)> ProcessFunction; +// A callback function for Calculator::Open, Process, or Close. +typedef std::function<::mediapipe::Status(CalculatorContext* cc)> + CalculatorContextFunction; + // A Calculator that runs a testing callback function in Process, -// which is specified as an input side packet. +// Open, or Close, which is specified as an input side packet. class LambdaCalculator : public CalculatorBase { public: static ::mediapipe::Status GetContract(CalculatorContract* cc) { @@ -561,21 +565,49 @@ class LambdaCalculator : public CalculatorBase { id < cc->Outputs().EndId(); ++id) { cc->Outputs().Get(id).SetAny(); } - cc->InputSidePackets().Index(0).Set(); + if (cc->InputSidePackets().HasTag("") > 0) { + cc->InputSidePackets().Tag("").Set(); + } + for (std::string tag : {"OPEN", "PROCESS", "CLOSE"}) { + if (cc->InputSidePackets().HasTag(tag)) { + cc->InputSidePackets().Tag(tag).Set(); + } + } return ::mediapipe::OkStatus(); } ::mediapipe::Status Open(CalculatorContext* cc) final { - callback_ = cc->InputSidePackets().Index(0).Get(); + if (cc->InputSidePackets().HasTag("OPEN")) { + return GetContextFn(cc, "OPEN")(cc); + } return ::mediapipe::OkStatus(); } ::mediapipe::Status Process(CalculatorContext* cc) final { - return callback_(cc->Inputs(), &(cc->Outputs())); + if (cc->InputSidePackets().HasTag("PROCESS")) { + return GetContextFn(cc, "PROCESS")(cc); + } + if (cc->InputSidePackets().HasTag("") > 0) { + return GetProcessFn(cc, "")(cc->Inputs(), &cc->Outputs()); + } + return ::mediapipe::OkStatus(); + } + + ::mediapipe::Status Close(CalculatorContext* cc) final { + if (cc->InputSidePackets().HasTag("CLOSE")) { + return GetContextFn(cc, "CLOSE")(cc); + } + return ::mediapipe::OkStatus(); } private: - ProcessFunction callback_; + ProcessFunction GetProcessFn(CalculatorContext* cc, std::string tag) { + return cc->InputSidePackets().Tag(tag).Get(); + } + CalculatorContextFunction GetContextFn(CalculatorContext* cc, + std::string tag) { + return cc->InputSidePackets().Tag(tag).Get(); + } }; REGISTER_CALCULATOR(LambdaCalculator); diff --git a/mediapipe/framework/testdata/BUILD b/mediapipe/framework/testdata/BUILD index 75ee0802d..599576899 100644 --- a/mediapipe/framework/testdata/BUILD +++ b/mediapipe/framework/testdata/BUILD @@ -55,6 +55,14 @@ proto_library( deps = ["@com_google_protobuf//:any_proto"], ) +mediapipe_cc_proto_library( + name = "zoo_mutator_cc_proto", + srcs = ["zoo_mutator.proto"], + cc_deps = ["@com_google_protobuf//:cc_wkt_protos"], + visibility = ["//mediapipe:__subpackages__"], + deps = [":zoo_mutator_proto"], +) + proto_library( name = "zoo_mutation_calculator_proto", srcs = ["zoo_mutation_calculator.proto"], diff --git a/mediapipe/framework/tool/subgraph_expansion.cc b/mediapipe/framework/tool/subgraph_expansion.cc index a91b20d92..665fd4cec 100644 --- a/mediapipe/framework/tool/subgraph_expansion.cc +++ b/mediapipe/framework/tool/subgraph_expansion.cc @@ -237,9 +237,9 @@ static ::mediapipe::Status PrefixNames(int subgraph_index, for (auto it = subgraph_nodes_start; it != nodes->end(); ++it) { const auto& node = *it; MP_RETURN_IF_ERROR(ValidateSubgraphFields(node)); - ASSIGN_OR_RETURN(auto subgraph, graph_registry->CreateByName( - config->package(), node.calculator(), - &node.options())); + ASSIGN_OR_RETURN(auto subgraph, + graph_registry->CreateByName(config->package(), + node.calculator(), &node)); MP_RETURN_IF_ERROR(PrefixNames(subgraph_counter++, &subgraph)); MP_RETURN_IF_ERROR(ConnectSubgraphStreams(node, &subgraph)); subgraphs.push_back(subgraph); diff --git a/mediapipe/framework/tool/subgraph_expansion_test.cc b/mediapipe/framework/tool/subgraph_expansion_test.cc index 8502d7461..8df5eb3c7 100644 --- a/mediapipe/framework/tool/subgraph_expansion_test.cc +++ b/mediapipe/framework/tool/subgraph_expansion_test.cc @@ -128,8 +128,8 @@ class NodeChainSubgraph : public Subgraph { public: ::mediapipe::StatusOr GetConfig( const SubgraphOptions& options) override { - const mediapipe::NodeChainSubgraphOptions& opts = - options.GetExtension(mediapipe::NodeChainSubgraphOptions::ext); + auto opts = + Subgraph::GetOptions(options); const ProtoString& node_type = opts.node_type(); int chain_length = opts.chain_length(); RET_CHECK(!node_type.empty()); diff --git a/mediapipe/gpu/BUILD b/mediapipe/gpu/BUILD index 46f2384e7..639ab9e24 100644 --- a/mediapipe/gpu/BUILD +++ b/mediapipe/gpu/BUILD @@ -80,8 +80,7 @@ GL_BASE_LINK_OPTS_OSS = GL_BASE_LINK_OPTS + select({ "-lEGL", ], "//mediapipe:android": [], - "//mediapipe:apple": [], - "//mediapipe:macos": [], + "//mediapipe:ios": [], ":disable_gpu": [], }) @@ -289,6 +288,25 @@ objc_library( ], ) +objc_library( + name = "MPPMetalUtil", + srcs = ["MPPMetalUtil.mm"], + hdrs = ["MPPMetalUtil.h"], + copts = [ + "-x objective-c++", + "-Wno-shorten-64-to-32", + ], + sdk_frameworks = [ + "CoreVideo", + "Metal", + ], + visibility = ["//visibility:public"], + deps = [ + "//mediapipe/objc:mediapipe_framework_ios", + "@google_toolbox_for_mac//:GTM_Defines", + ], +) + proto_library( name = "gl_context_options_proto", srcs = ["gl_context_options.proto"], @@ -499,6 +517,7 @@ cc_library( ":gl_base", ":gl_context", ":gpu_buffer", + ":gpu_buffer_format", ":gpu_buffer_multi_pool", ":gpu_shared_data_internal", ":gpu_service", diff --git a/mediapipe/gpu/MPPMetalUtil.h b/mediapipe/gpu/MPPMetalUtil.h new file mode 100644 index 000000000..328cb99fa --- /dev/null +++ b/mediapipe/gpu/MPPMetalUtil.h @@ -0,0 +1,49 @@ +// 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_GPU_MPP_METAL_UTIL_H_ +#define MEDIAPIPE_GPU_MPP_METAL_UTIL_H_ + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface MPPMetalUtil : NSObject { +} + +/// Copies a Metal Buffer from source to destination. +/// Uses blitCommandEncoder and assumes offset of 0. ++ (void)blitMetalBufferTo:(id)destination + from:(id)source + blocking:(bool)blocking + commandBuffer:(id)commandBuffer; + +/// Copies a Metal Buffer from source to destination. +/// Simple wrapper for blitCommandEncoder. +/// Optionally block until operation is completed. ++ (void)blitMetalBufferTo:(id)destination + destinationOffset:(int)destinationOffset + from:(id)source + sourceOffset:(int)sourceOffset + bytes:(size_t)bytes + blocking:(bool)blocking + commandBuffer:(id)commandBuffer; + +@end + +NS_ASSUME_NONNULL_END + +#endif // MEDIAPIPE_GPU_MPP_METAL_UTIL_H_ diff --git a/mediapipe/gpu/MPPMetalUtil.mm b/mediapipe/gpu/MPPMetalUtil.mm new file mode 100644 index 000000000..81cd8a358 --- /dev/null +++ b/mediapipe/gpu/MPPMetalUtil.mm @@ -0,0 +1,51 @@ +// 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. + +#import "mediapipe/gpu/MPPMetalUtil.h" + +@implementation MPPMetalUtil + ++ (void)blitMetalBufferTo:(id)destination + from:(id)source + blocking:(bool)blocking + commandBuffer:(id)commandBuffer { + size_t bytes = MIN(destination.length, source.length); + [self blitMetalBufferTo:destination + destinationOffset:0 + from:source + sourceOffset:0 + bytes:bytes + blocking:blocking + commandBuffer:commandBuffer]; +} + ++ (void)blitMetalBufferTo:(id)destination + destinationOffset:(int)destinationOffset + from:(id)source + sourceOffset:(int)sourceOffset + bytes:(size_t)bytes + blocking:(bool)blocking + commandBuffer:(id)commandBuffer { + id blit_command = [commandBuffer blitCommandEncoder]; + [blit_command copyFromBuffer:source + sourceOffset:sourceOffset + toBuffer:destination + destinationOffset:destinationOffset + size:bytes]; + [blit_command endEncoding]; + [commandBuffer commit]; + if (blocking) [commandBuffer waitUntilCompleted]; +} + +@end diff --git a/mediapipe/gpu/gl_calculator_helper_impl.h b/mediapipe/gpu/gl_calculator_helper_impl.h index 1c80917e2..3d92ca671 100644 --- a/mediapipe/gpu/gl_calculator_helper_impl.h +++ b/mediapipe/gpu/gl_calculator_helper_impl.h @@ -73,7 +73,7 @@ class GlCalculatorHelperImpl { #endif // !MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER // Sets default texture filtering parameters. - void SetStandardTextureParams(GLenum target); + void SetStandardTextureParams(GLenum target, GLint internal_format); // Create the framebuffer for rendering. void CreateFramebuffer(); diff --git a/mediapipe/gpu/gl_calculator_helper_impl_common.cc b/mediapipe/gpu/gl_calculator_helper_impl_common.cc index b42618f77..cf2dcf582 100644 --- a/mediapipe/gpu/gl_calculator_helper_impl_common.cc +++ b/mediapipe/gpu/gl_calculator_helper_impl_common.cc @@ -13,6 +13,7 @@ // limitations under the License. #include "mediapipe/gpu/gl_calculator_helper_impl.h" +#include "mediapipe/gpu/gpu_buffer_format.h" #include "mediapipe/gpu/gpu_shared_data_internal.h" namespace mediapipe { @@ -86,9 +87,21 @@ void GlCalculatorHelperImpl::BindFramebuffer(const GlTexture& dst) { #endif } -void GlCalculatorHelperImpl::SetStandardTextureParams(GLenum target) { - glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_LINEAR); +void GlCalculatorHelperImpl::SetStandardTextureParams(GLenum target, + GLint internal_format) { + GLint filter; + switch (internal_format) { + case GL_R32F: + case GL_RGBA32F: + // 32F (unlike 16f) textures do not support texture filtering + // (According to OpenGL ES specification [TEXTURE IMAGE SPECIFICATION]) + filter = GL_NEAREST; + break; + default: + filter = GL_LINEAR; + } + glTexParameteri(target, GL_TEXTURE_MIN_FILTER, filter); + glTexParameteri(target, GL_TEXTURE_MAG_FILTER, filter); glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); } @@ -136,7 +149,9 @@ GlTexture GlCalculatorHelperImpl::MapGlTextureBuffer( // TODO: do the params need to be reset here?? glBindTexture(texture.target(), texture.name()); - SetStandardTextureParams(texture.target()); + GlTextureInfo info = + GlTextureInfoForGpuBufferFormat(texture_buffer->format(), texture.plane_); + SetStandardTextureParams(texture.target(), info.gl_internal_format); glBindTexture(texture.target(), 0); return texture; @@ -150,7 +165,9 @@ GlTextureBufferSharedPtr GlCalculatorHelperImpl::MakeGlTextureBuffer( GpuBufferFormatForImageFormat(image_frame.Format()), image_frame.PixelData()); glBindTexture(GL_TEXTURE_2D, buffer->name_); - SetStandardTextureParams(buffer->target_); + GlTextureInfo info = + GlTextureInfoForGpuBufferFormat(buffer->format_, /*plane=*/0); + SetStandardTextureParams(buffer->target_, info.gl_internal_format); glBindTexture(GL_TEXTURE_2D, 0); return buffer; diff --git a/mediapipe/gpu/gl_calculator_helper_impl_ios.mm b/mediapipe/gpu/gl_calculator_helper_impl_ios.mm index d62a1f90d..00b2e643c 100644 --- a/mediapipe/gpu/gl_calculator_helper_impl_ios.mm +++ b/mediapipe/gpu/gl_calculator_helper_impl_ios.mm @@ -54,7 +54,7 @@ GlTexture GlCalculatorHelperImpl::CreateSourceTexture( glTexImage2D(GL_TEXTURE_2D, 0, info.gl_internal_format, texture.width_, texture.height_, 0, info.gl_format, info.gl_type, image_frame.PixelData()); - SetStandardTextureParams(GL_TEXTURE_2D); + SetStandardTextureParams(GL_TEXTURE_2D, info.gl_internal_format); return texture; } @@ -107,7 +107,7 @@ GlTexture GlCalculatorHelperImpl::MapGpuBuffer( #endif // TARGET_OS_OSX glBindTexture(texture.target(), texture.name()); - SetStandardTextureParams(texture.target()); + SetStandardTextureParams(texture.target(), info.gl_internal_format); return texture; } diff --git a/mediapipe/gpu/gl_context_egl.cc b/mediapipe/gpu/gl_context_egl.cc index 5a57b32db..79f0c30eb 100644 --- a/mediapipe/gpu/gl_context_egl.cc +++ b/mediapipe/gpu/gl_context_egl.cc @@ -110,7 +110,13 @@ GlContext::StatusOrGlContext GlContext::Create(EGLContext share_context, eglChooseConfig(display_, config_attr, &config_, 1, &num_configs); if (!success) { return ::mediapipe::UnknownErrorBuilder(MEDIAPIPE_LOC) - << "eglChooseConfig() returned error " << eglGetError(); + << "eglChooseConfig() returned error " << std::showbase << std::hex + << eglGetError(); + } + if (!num_configs) { + return ::mediapipe::UnknownErrorBuilder(MEDIAPIPE_LOC) + << "eglChooseConfig() returned no matching EGL configuration for " + << "RGBA8888 D16 ES" << gl_version << " request. "; } const EGLint context_attr[] = { @@ -125,7 +131,8 @@ GlContext::StatusOrGlContext GlContext::Create(EGLContext share_context, int error = eglGetError(); RET_CHECK(context_ != EGL_NO_CONTEXT) << "Could not create GLES " << gl_version << " context; " - << "eglCreateContext() returned error " << error + << "eglCreateContext() returned error " << std::showbase << std::hex + << error << (error == EGL_BAD_CONTEXT ? ": external context uses a different version of OpenGL" : ""); @@ -143,7 +150,8 @@ GlContext::StatusOrGlContext GlContext::Create(EGLContext share_context, display_ = eglGetDisplay(EGL_DEFAULT_DISPLAY); RET_CHECK(display_ != EGL_NO_DISPLAY) - << "eglGetDisplay() returned error " << eglGetError(); + << "eglGetDisplay() returned error " << std::showbase << std::hex + << eglGetError(); EGLBoolean success = eglInitialize(display_, &major, &minor); RET_CHECK(success) << "Unable to initialize EGL"; @@ -162,7 +170,8 @@ GlContext::StatusOrGlContext GlContext::Create(EGLContext share_context, surface_ = eglCreatePbufferSurface(display_, config_, pbuffer_attr); RET_CHECK(surface_ != EGL_NO_SURFACE) - << "eglCreatePbufferSurface() returned error " << eglGetError(); + << "eglCreatePbufferSurface() returned error " << std::showbase + << std::hex << eglGetError(); return ::mediapipe::OkStatus(); } @@ -186,17 +195,21 @@ void GlContext::DestroyContext() { if (IsCurrent()) { if (!eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) { - LOG(ERROR) << "eglMakeCurrent() returned error " << eglGetError(); + LOG(ERROR) << "eglMakeCurrent() returned error " << std::showbase + << std::hex << eglGetError(); } } if (surface_ != EGL_NO_SURFACE) { if (!eglDestroySurface(display_, surface_)) { - LOG(ERROR) << "eglDestroySurface() returned error " << eglGetError(); + LOG(ERROR) << "eglDestroySurface() returned error " << std::showbase + << std::hex << eglGetError(); } + surface_ = EGL_NO_SURFACE; } if (context_ != EGL_NO_CONTEXT) { if (!eglDestroyContext(display_, context_)) { - LOG(ERROR) << "eglDestroyContext() returned error " << eglGetError(); + LOG(ERROR) << "eglDestroyContext() returned error " << std::showbase + << std::hex << eglGetError(); } context_ = EGL_NO_CONTEXT; } @@ -245,7 +258,8 @@ void GlContext::GetCurrentContextBinding(GlContext::ContextBinding* binding) { EGLBoolean success = eglMakeCurrent(display, new_binding.draw_surface, new_binding.read_surface, new_binding.context); - RET_CHECK(success) << "eglMakeCurrent() returned error " << eglGetError(); + RET_CHECK(success) << "eglMakeCurrent() returned error " << std::showbase + << std::hex << eglGetError(); return ::mediapipe::OkStatus(); } diff --git a/mediapipe/gpu/gl_texture_buffer.cc b/mediapipe/gpu/gl_texture_buffer.cc index 80cfd20d5..e8dbda3e3 100644 --- a/mediapipe/gpu/gl_texture_buffer.cc +++ b/mediapipe/gpu/gl_texture_buffer.cc @@ -77,7 +77,8 @@ bool GlTextureBuffer::CreateInternal(const void* data) { // TODO: maybe we do not actually have to wait for the // consumer sync here. Check docs. sync_token->WaitOnGpu(); - DCHECK(glIsTexture(name_to_delete)); + DLOG_IF(ERROR, !glIsTexture(name_to_delete)) + << "Deleting invalid texture id: " << name_to_delete; glDeleteTextures(1, &name_to_delete); }); }; diff --git a/mediapipe/gpu/gpu_buffer_test.cc b/mediapipe/gpu/gpu_buffer_test.cc new file mode 100644 index 000000000..a4bd93a7a --- /dev/null +++ b/mediapipe/gpu/gpu_buffer_test.cc @@ -0,0 +1,50 @@ +// 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 "mediapipe/gpu/gpu_buffer.h" + +#include "mediapipe/framework/port/gmock.h" +#include "mediapipe/framework/port/gtest.h" +#include "mediapipe/gpu/gpu_test_base.h" + +namespace mediapipe { +namespace { + +class GpuBufferTest : public GpuTestBase {}; + +TEST_F(GpuBufferTest, BasicTest) { + RunInGlContext([this] { + GpuBuffer buffer = gpu_shared_.gpu_buffer_pool.GetBuffer(300, 200); + EXPECT_EQ(buffer.width(), 300); + EXPECT_EQ(buffer.height(), 200); + EXPECT_TRUE(buffer); + EXPECT_FALSE(buffer == nullptr); + + GpuBuffer no_buffer; + EXPECT_FALSE(no_buffer); + EXPECT_TRUE(no_buffer == nullptr); + + GpuBuffer buffer2 = buffer; + EXPECT_EQ(buffer, buffer); + EXPECT_EQ(buffer, buffer2); + EXPECT_NE(buffer, no_buffer); + + buffer = nullptr; + EXPECT_TRUE(buffer == nullptr); + EXPECT_TRUE(buffer == no_buffer); + }); +} + +} // anonymous namespace +} // namespace mediapipe diff --git a/mediapipe/gpu/gpu_shared_data_internal.h b/mediapipe/gpu/gpu_shared_data_internal.h index 65f9d8891..b829c4f63 100644 --- a/mediapipe/gpu/gpu_shared_data_internal.h +++ b/mediapipe/gpu/gpu_shared_data_internal.h @@ -123,7 +123,7 @@ struct GpuSharedData { PlatformGlContext external_context) { auto status_or_resources = GpuResources::Create(external_context); MEDIAPIPE_CHECK_OK(status_or_resources.status()) - << "could not create GpuResources"; + << ": could not create GpuResources"; return std::move(status_or_resources).ValueOrDie(); } }; diff --git a/mediapipe/gpu/gpu_test_base.h b/mediapipe/gpu/gpu_test_base.h new file mode 100644 index 000000000..e9fd64725 --- /dev/null +++ b/mediapipe/gpu/gpu_test_base.h @@ -0,0 +1,39 @@ +// 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_GPU_GPU_TEST_BASE_H_ +#define MEDIAPIPE_GPU_GPU_TEST_BASE_H_ + +#include "mediapipe/framework/port/gmock.h" +#include "mediapipe/framework/port/gtest.h" +#include "mediapipe/gpu/gl_calculator_helper.h" +#include "mediapipe/gpu/gpu_shared_data_internal.h" + +namespace mediapipe { + +class GpuTestBase : public ::testing::Test { + protected: + GpuTestBase() { helper_.InitializeForTest(&gpu_shared_); } + + void RunInGlContext(std::function gl_func) { + helper_.RunInGlContext(std::move(gl_func)); + } + + GpuSharedData gpu_shared_; + GlCalculatorHelper helper_; +}; + +} // namespace mediapipe + +#endif // MEDIAPIPE_GPU_GPU_TEST_BASE_H_ diff --git a/mediapipe/graphs/face_detection/BUILD b/mediapipe/graphs/face_detection/BUILD index 0281f3437..ccc9995d6 100644 --- a/mediapipe/graphs/face_detection/BUILD +++ b/mediapipe/graphs/face_detection/BUILD @@ -35,6 +35,23 @@ cc_library( ], ) +cc_library( + name = "desktop_tflite_calculators", + deps = [ + "//mediapipe/calculators/core:flow_limiter_calculator", + "//mediapipe/calculators/image:image_transformation_calculator", + "//mediapipe/calculators/tflite:ssd_anchors_calculator", + "//mediapipe/calculators/tflite:tflite_converter_calculator", + "//mediapipe/calculators/tflite:tflite_inference_calculator", + "//mediapipe/calculators/tflite:tflite_tensors_to_detections_calculator", + "//mediapipe/calculators/util:annotation_overlay_calculator", + "//mediapipe/calculators/util:detection_label_id_to_text_calculator", + "//mediapipe/calculators/util:detection_letterbox_removal_calculator", + "//mediapipe/calculators/util:detections_to_render_data_calculator", + "//mediapipe/calculators/util:non_max_suppression_calculator", + ], +) + load( "//mediapipe/framework/tool:mediapipe_graph.bzl", "mediapipe_binary_graph", diff --git a/mediapipe/graphs/face_detection/face_detection_desktop_live.pbtxt b/mediapipe/graphs/face_detection/face_detection_desktop_live.pbtxt new file mode 100644 index 000000000..95fdb3623 --- /dev/null +++ b/mediapipe/graphs/face_detection/face_detection_desktop_live.pbtxt @@ -0,0 +1,184 @@ +# MediaPipe graph that performs face detection with TensorFlow Lite on CPU. +# Used in the examples in +# mediapipie/examples/desktop/face_detection:face_detection_cpu. + +# Images on GPU coming into and out of the graph. +input_stream: "input_video" +output_stream: "output_video" + +# Throttles the images flowing downstream for flow control. It passes through +# the very first incoming image unaltered, and waits for +# TfLiteTensorsToDetectionsCalculator downstream in the graph to finish +# generating the corresponding detections before it passes through another +# image. All images that come in while waiting are dropped, limiting the number +# of in-flight images between this calculator and +# TfLiteTensorsToDetectionsCalculator to 1. This prevents the nodes in between +# from queuing up incoming images and data excessively, which leads to increased +# latency and memory usage, unwanted in real-time mobile applications. It also +# eliminates unnecessarily computation, e.g., a transformed image produced by +# ImageTransformationCalculator may get dropped downstream if the subsequent +# TfLiteConverterCalculator or TfLiteInferenceCalculator is still busy +# processing previous inputs. +node { + calculator: "FlowLimiterCalculator" + input_stream: "input_video" + input_stream: "FINISHED:detections" + input_stream_info: { + tag_index: "FINISHED" + back_edge: true + } + output_stream: "throttled_input_video" +} + +# Transforms the input image on CPU to a 128x128 image. To scale the input +# image, the scale_mode option is set to FIT to preserve the aspect ratio, +# resulting in potential letterboxing in the transformed image. +node: { + calculator: "ImageTransformationCalculator" + input_stream: "IMAGE:throttled_input_video" + output_stream: "IMAGE:transformed_input_video_cpu" + output_stream: "LETTERBOX_PADDING:letterbox_padding" + node_options: { + [type.googleapis.com/mediapipe.ImageTransformationCalculatorOptions] { + output_width: 128 + output_height: 128 + scale_mode: FIT + } + } +} + +# Converts the transformed input image on CPU into an image tensor stored as a +# TfLiteTensor. +node { + calculator: "TfLiteConverterCalculator" + input_stream: "IMAGE:transformed_input_video_cpu" + output_stream: "TENSORS:image_tensor" +} + +# Runs a TensorFlow Lite model on CPU that takes an image tensor and outputs a +# vector of tensors representing, for instance, detection boxes/keypoints and +# scores. +node { + calculator: "TfLiteInferenceCalculator" + input_stream: "TENSORS:image_tensor" + output_stream: "TENSORS:detection_tensors" + node_options: { + [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] { + model_path: "mediapipe/models/face_detection_front.tflite" + } + } +} + +# Generates a single side packet containing a vector of SSD anchors based on +# the specification in the options. +node { + calculator: "SsdAnchorsCalculator" + output_side_packet: "anchors" + node_options: { + [type.googleapis.com/mediapipe.SsdAnchorsCalculatorOptions] { + num_layers: 4 + min_scale: 0.1484375 + max_scale: 0.75 + input_size_height: 128 + input_size_width: 128 + anchor_offset_x: 0.5 + anchor_offset_y: 0.5 + strides: 8 + strides: 16 + strides: 16 + strides: 16 + aspect_ratios: 1.0 + fixed_anchor_size: true + } + } +} + +# Decodes the detection tensors generated by the TensorFlow Lite model, based on +# the SSD anchors and the specification in the options, into a vector of +# detections. Each detection describes a detected object. +node { + calculator: "TfLiteTensorsToDetectionsCalculator" + input_stream: "TENSORS:detection_tensors" + input_side_packet: "ANCHORS:anchors" + output_stream: "DETECTIONS:detections" + node_options: { + [type.googleapis.com/mediapipe.TfLiteTensorsToDetectionsCalculatorOptions] { + num_classes: 1 + num_boxes: 896 + num_coords: 16 + box_coord_offset: 0 + keypoint_coord_offset: 4 + num_keypoints: 6 + num_values_per_keypoint: 2 + sigmoid_score: true + score_clipping_thresh: 100.0 + reverse_output_order: true + x_scale: 128.0 + y_scale: 128.0 + h_scale: 128.0 + w_scale: 128.0 + min_score_thresh: 0.75 + } + } +} + +# Performs non-max suppression to remove excessive detections. +node { + calculator: "NonMaxSuppressionCalculator" + input_stream: "detections" + output_stream: "filtered_detections" + node_options: { + [type.googleapis.com/mediapipe.NonMaxSuppressionCalculatorOptions] { + min_suppression_threshold: 0.3 + overlap_type: INTERSECTION_OVER_UNION + algorithm: WEIGHTED + return_empty_detections: true + } + } +} + +# Maps detection label IDs to the corresponding label text ("Face"). The label +# map is provided in the label_map_path option. +node { + calculator: "DetectionLabelIdToTextCalculator" + input_stream: "filtered_detections" + output_stream: "labeled_detections" + node_options: { + [type.googleapis.com/mediapipe.DetectionLabelIdToTextCalculatorOptions] { + label_map_path: "mediapipe/models/face_detection_front_labelmap.txt" + } + } +} + +# Adjusts detection locations (already normalized to [0.f, 1.f]) on the +# letterboxed image (after image transformation with the FIT scale mode) to the +# corresponding locations on the same image with the letterbox removed (the +# input image to the graph before image transformation). +node { + calculator: "DetectionLetterboxRemovalCalculator" + input_stream: "DETECTIONS:labeled_detections" + input_stream: "LETTERBOX_PADDING:letterbox_padding" + output_stream: "DETECTIONS:output_detections" +} + +# Converts the detections to drawing primitives for annotation overlay. +node { + calculator: "DetectionsToRenderDataCalculator" + input_stream: "DETECTIONS:output_detections" + output_stream: "RENDER_DATA:render_data" + node_options: { + [type.googleapis.com/mediapipe.DetectionsToRenderDataCalculatorOptions] { + thickness: 4.0 + color { r: 255 g: 0 b: 0 } + } + } +} + +# Draws annotations and overlays them on top of the input images. +node { + calculator: "AnnotationOverlayCalculator" + input_stream: "INPUT_FRAME:throttled_input_video" + input_stream: "render_data" + output_stream: "OUTPUT_FRAME:output_video" +} + diff --git a/mediapipe/graphs/face_detection/face_detection_mobile_cpu.pbtxt b/mediapipe/graphs/face_detection/face_detection_mobile_cpu.pbtxt index d52ef6c5c..1b6ecbf47 100644 --- a/mediapipe/graphs/face_detection/face_detection_mobile_cpu.pbtxt +++ b/mediapipe/graphs/face_detection/face_detection_mobile_cpu.pbtxt @@ -41,7 +41,7 @@ node: { output_stream: "input_video_cpu" } -# Transforms the input image on GPU to a 128x128 image. To scale the input +# Transforms the input image on CPU to a 128x128 image. To scale the input # image, the scale_mode option is set to FIT to preserve the aspect ratio, # resulting in potential letterboxing in the transformed image. node: { @@ -75,7 +75,7 @@ node { output_stream: "TENSORS:detection_tensors" node_options: { [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] { - model_path: "face_detection_front.tflite" + model_path: "mediapipe/models/face_detection_front.tflite" } } } @@ -156,7 +156,7 @@ node { output_stream: "labeled_detections" node_options: { [type.googleapis.com/mediapipe.DetectionLabelIdToTextCalculatorOptions] { - label_map_path: "face_detection_front_labelmap.txt" + label_map_path: "mediapipe/models/face_detection_front_labelmap.txt" } } } @@ -179,7 +179,7 @@ node { output_stream: "RENDER_DATA:render_data" node_options: { [type.googleapis.com/mediapipe.DetectionsToRenderDataCalculatorOptions] { - thickness: 10.0 + thickness: 4.0 color { r: 255 g: 0 b: 0 } } } diff --git a/mediapipe/graphs/face_detection/face_detection_mobile_gpu.pbtxt b/mediapipe/graphs/face_detection/face_detection_mobile_gpu.pbtxt index e12787d5b..2fb85bc00 100644 --- a/mediapipe/graphs/face_detection/face_detection_mobile_gpu.pbtxt +++ b/mediapipe/graphs/face_detection/face_detection_mobile_gpu.pbtxt @@ -62,10 +62,10 @@ node { node { calculator: "TfLiteInferenceCalculator" input_stream: "TENSORS_GPU:image_tensor" - output_stream: "TENSORS:detection_tensors" + output_stream: "TENSORS_GPU:detection_tensors" node_options: { [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] { - model_path: "face_detection_front.tflite" + model_path: "mediapipe/models/face_detection_front.tflite" } } } @@ -99,7 +99,7 @@ node { # detections. Each detection describes a detected object. node { calculator: "TfLiteTensorsToDetectionsCalculator" - input_stream: "TENSORS:detection_tensors" + input_stream: "TENSORS_GPU:detection_tensors" input_side_packet: "ANCHORS:anchors" output_stream: "DETECTIONS:detections" node_options: { @@ -146,7 +146,7 @@ node { output_stream: "labeled_detections" node_options: { [type.googleapis.com/mediapipe.DetectionLabelIdToTextCalculatorOptions] { - label_map_path: "face_detection_front_labelmap.txt" + label_map_path: "mediapipe/models/face_detection_front_labelmap.txt" } } } @@ -169,7 +169,7 @@ node { output_stream: "RENDER_DATA:render_data" node_options: { [type.googleapis.com/mediapipe.DetectionsToRenderDataCalculatorOptions] { - thickness: 10.0 + thickness: 4.0 color { r: 255 g: 0 b: 0 } } } diff --git a/mediapipe/graphs/hair_segmentation/hair_segmentation_mobile_gpu.pbtxt b/mediapipe/graphs/hair_segmentation/hair_segmentation_mobile_gpu.pbtxt index ed5d0ada4..c8db44d40 100644 --- a/mediapipe/graphs/hair_segmentation/hair_segmentation_mobile_gpu.pbtxt +++ b/mediapipe/graphs/hair_segmentation/hair_segmentation_mobile_gpu.pbtxt @@ -111,7 +111,7 @@ node { input_side_packet: "CUSTOM_OP_RESOLVER:op_resolver" node_options: { [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] { - model_path: "hair_segmentation.tflite" + model_path: "mediapipe/models/hair_segmentation.tflite" use_gpu: true } } diff --git a/mediapipe/graphs/hand_tracking/BUILD b/mediapipe/graphs/hand_tracking/BUILD index 73c5a6ce3..09a8e4d0f 100644 --- a/mediapipe/graphs/hand_tracking/BUILD +++ b/mediapipe/graphs/hand_tracking/BUILD @@ -19,73 +19,35 @@ package(default_visibility = ["//visibility:public"]) load( "//mediapipe/framework/tool:mediapipe_graph.bzl", "mediapipe_binary_graph", - "mediapipe_simple_subgraph", ) -mediapipe_simple_subgraph( - name = "hand_detection_gpu", - graph = "hand_detection_gpu.pbtxt", - register_as = "HandDetectionSubgraph", +cc_library( + name = "desktop_tflite_calculators", deps = [ - "//mediapipe/calculators/image:image_properties_calculator", - "//mediapipe/calculators/image:image_transformation_calculator", - "//mediapipe/calculators/tflite:ssd_anchors_calculator", - "//mediapipe/calculators/tflite:tflite_converter_calculator", - "//mediapipe/calculators/tflite:tflite_custom_op_resolver_calculator", - "//mediapipe/calculators/tflite:tflite_inference_calculator", - "//mediapipe/calculators/tflite:tflite_tensors_to_detections_calculator", - "//mediapipe/calculators/util:detection_label_id_to_text_calculator", - "//mediapipe/calculators/util:detection_letterbox_removal_calculator", - "//mediapipe/calculators/util:detections_to_rects_calculator", - "//mediapipe/calculators/util:non_max_suppression_calculator", - "//mediapipe/calculators/util:rect_transformation_calculator", - ], -) - -mediapipe_simple_subgraph( - name = "hand_landmark_gpu", - graph = "hand_landmark_gpu.pbtxt", - register_as = "HandLandmarkSubgraph", - deps = [ - "//mediapipe/calculators/core:split_vector_calculator", - "//mediapipe/calculators/image:image_cropping_calculator", - "//mediapipe/calculators/image:image_properties_calculator", - "//mediapipe/calculators/image:image_transformation_calculator", - "//mediapipe/calculators/tflite:tflite_converter_calculator", - "//mediapipe/calculators/tflite:tflite_inference_calculator", - "//mediapipe/calculators/tflite:tflite_tensors_to_floats_calculator", - "//mediapipe/calculators/tflite:tflite_tensors_to_landmarks_calculator", - "//mediapipe/calculators/util:detections_to_rects_calculator", - "//mediapipe/calculators/util:landmark_letterbox_removal_calculator", - "//mediapipe/calculators/util:landmark_projection_calculator", - "//mediapipe/calculators/util:landmarks_to_detection_calculator", - "//mediapipe/calculators/util:rect_transformation_calculator", - "//mediapipe/calculators/util:thresholding_calculator", - ], -) - -mediapipe_simple_subgraph( - name = "renderer_gpu", - graph = "renderer_gpu.pbtxt", - register_as = "RendererSubgraph", - deps = [ - "//mediapipe/calculators/util:annotation_overlay_calculator", - "//mediapipe/calculators/util:detections_to_render_data_calculator", - "//mediapipe/calculators/util:landmarks_to_render_data_calculator", - "//mediapipe/calculators/util:rect_to_render_data_calculator", + "//mediapipe/calculators/core:flow_limiter_calculator", + "//mediapipe/calculators/core:gate_calculator", + "//mediapipe/calculators/core:immediate_mux_calculator", + "//mediapipe/calculators/core:merge_calculator", + "//mediapipe/calculators/core:packet_inner_join_calculator", + "//mediapipe/calculators/core:previous_loopback_calculator", + "//mediapipe/calculators/video:opencv_video_decoder_calculator", + "//mediapipe/calculators/video:opencv_video_encoder_calculator", + "//mediapipe/graphs/hand_tracking/subgraphs:hand_detection_cpu", + "//mediapipe/graphs/hand_tracking/subgraphs:hand_landmark_cpu", + "//mediapipe/graphs/hand_tracking/subgraphs:renderer_cpu", ], ) cc_library( name = "mobile_calculators", deps = [ - ":hand_detection_gpu", - ":hand_landmark_gpu", - ":renderer_gpu", "//mediapipe/calculators/core:flow_limiter_calculator", "//mediapipe/calculators/core:gate_calculator", "//mediapipe/calculators/core:merge_calculator", "//mediapipe/calculators/core:previous_loopback_calculator", + "//mediapipe/graphs/hand_tracking/subgraphs:hand_detection_gpu", + "//mediapipe/graphs/hand_tracking/subgraphs:hand_landmark_gpu", + "//mediapipe/graphs/hand_tracking/subgraphs:renderer_gpu", ], ) @@ -99,9 +61,9 @@ mediapipe_binary_graph( cc_library( name = "detection_mobile_calculators", deps = [ - ":hand_detection_gpu", - ":renderer_gpu", "//mediapipe/calculators/core:flow_limiter_calculator", + "//mediapipe/graphs/hand_tracking/subgraphs:hand_detection_gpu", + "//mediapipe/graphs/hand_tracking/subgraphs:renderer_gpu", ], ) diff --git a/mediapipe/graphs/hand_tracking/hand_detection_desktop.pbtxt b/mediapipe/graphs/hand_tracking/hand_detection_desktop.pbtxt new file mode 100644 index 000000000..ac8e7a401 --- /dev/null +++ b/mediapipe/graphs/hand_tracking/hand_detection_desktop.pbtxt @@ -0,0 +1,62 @@ +# MediaPipe graph that performs hand detection on desktop with TensorFlow Lite +# on CPU. +# Used in the example in +# mediapipie/examples/desktop/hand_tracking:hand_detection_tflite. + +# max_queue_size limits the number of packets enqueued on any input stream +# by throttling inputs to the graph. This makes the graph only process one +# frame per time. +max_queue_size: 1 + +# Decodes an input video file into images and a video header. +node { + calculator: "OpenCvVideoDecoderCalculator" + input_side_packet: "INPUT_FILE_PATH:input_video_path" + output_stream: "VIDEO:input_video" + output_stream: "VIDEO_PRESTREAM:input_video_header" +} + +# Performs hand detection model on the input frames. See +# hand_detection_cpu.pbtxt for the detail of the sub-graph. +node { + calculator: "HandDetectionSubgraph" + input_stream: "input_video" + output_stream: "DETECTIONS:output_detections" +} + +# Converts the detections to drawing primitives for annotation overlay. +node { + calculator: "DetectionsToRenderDataCalculator" + input_stream: "DETECTIONS:output_detections" + output_stream: "RENDER_DATA:render_data" + node_options: { + [type.googleapis.com/mediapipe.DetectionsToRenderDataCalculatorOptions] { + thickness: 4.0 + color { r: 0 g: 255 b: 0 } + } + } +} + +# Draws annotations and overlays them on top of the original image coming into +# the graph. +node { + calculator: "AnnotationOverlayCalculator" + input_stream: "INPUT_FRAME:input_video" + input_stream: "render_data" + output_stream: "OUTPUT_FRAME:output_video" +} + +# Encodes the annotated images into a video file, adopting properties specified +# in the input video header, e.g., video framerate. +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/hand_tracking/hand_detection_desktop_live.pbtxt b/mediapipe/graphs/hand_tracking/hand_detection_desktop_live.pbtxt new file mode 100644 index 000000000..9e6fdad06 --- /dev/null +++ b/mediapipe/graphs/hand_tracking/hand_detection_desktop_live.pbtxt @@ -0,0 +1,38 @@ +# MediaPipe graph that performs hand detection on desktop with TensorFlow Lite +# on CPU. +# Used in the example in +# mediapipie/examples/desktop/hand_tracking:hand_detection_cpu. + +# Images coming into and out of the graph. +input_stream: "input_video" +output_stream: "output_video" + +# Performs hand detection model on the input frames. See +# hand_detection_cpu.pbtxt for the detail of the sub-graph. +node { + calculator: "HandDetectionSubgraph" + input_stream: "input_video" + output_stream: "DETECTIONS:output_detections" +} + +# Converts the detections to drawing primitives for annotation overlay. +node { + calculator: "DetectionsToRenderDataCalculator" + input_stream: "DETECTIONS:output_detections" + output_stream: "RENDER_DATA:render_data" + node_options: { + [type.googleapis.com/mediapipe.DetectionsToRenderDataCalculatorOptions] { + thickness: 4.0 + color { r: 0 g: 255 b: 0 } + } + } +} + +# Draws annotations and overlays them on top of the original image coming into +# the graph. +node { + calculator: "AnnotationOverlayCalculator" + input_stream: "INPUT_FRAME:input_video" + input_stream: "render_data" + output_stream: "OUTPUT_FRAME:output_video" +} diff --git a/mediapipe/graphs/hand_tracking/hand_tracking_desktop.pbtxt b/mediapipe/graphs/hand_tracking/hand_tracking_desktop.pbtxt new file mode 100644 index 000000000..29ad822a8 --- /dev/null +++ b/mediapipe/graphs/hand_tracking/hand_tracking_desktop.pbtxt @@ -0,0 +1,126 @@ +# MediaPipe graph that performs hand tracking on desktop with TensorFlow Lite +# on CPU. +# Used in the example in +# mediapipie/examples/desktop/hand_tracking:hand_tracking_tflite. + +# max_queue_size limits the number of packets enqueued on any input stream +# by throttling inputs to the graph. This makes the graph only process one +# frame per time. +max_queue_size: 1 + +# Decodes an input video file into images and a video header. +node { + calculator: "OpenCvVideoDecoderCalculator" + input_side_packet: "INPUT_FILE_PATH:input_video_path" + output_stream: "VIDEO:input_video" + output_stream: "VIDEO_PRESTREAM:input_video_header" +} + +# Caches a hand-presence decision fed back from HandLandmarkSubgraph, and upon +# the arrival of the next input image sends out the cached decision with the +# timestamp replaced by that of the input image, essentially generating a packet +# that carries the previous hand-presence decision. Note that upon the arrival +# of the very first input image, an empty packet is sent out to jump start the +# feedback loop. +node { + calculator: "PreviousLoopbackCalculator" + input_stream: "MAIN:input_video" + input_stream: "LOOP:hand_presence" + input_stream_info: { + tag_index: "LOOP" + back_edge: true + } + output_stream: "PREV_LOOP:prev_hand_presence" +} + +# Drops the incoming image if HandLandmarkSubgraph was able to identify hand +# presence in the previous image. Otherwise, passes the incoming image through +# to trigger a new round of hand detection in HandDetectionSubgraph. +node { + calculator: "GateCalculator" + input_stream: "input_video" + input_stream: "DISALLOW:prev_hand_presence" + output_stream: "hand_detection_input_video" + + node_options: { + [type.googleapis.com/mediapipe.GateCalculatorOptions] { + empty_packets_as_allow: true + } + } +} + +# Subgraph that detections hands (see hand_detection_cpu.pbtxt). +node { + calculator: "HandDetectionSubgraph" + input_stream: "hand_detection_input_video" + output_stream: "DETECTIONS:palm_detections" + output_stream: "NORM_RECT:hand_rect_from_palm_detections" +} + +# Subgraph that localizes hand landmarks (see hand_landmark_cpu.pbtxt). +node { + calculator: "HandLandmarkSubgraph" + input_stream: "IMAGE:input_video" + input_stream: "NORM_RECT:hand_rect" + output_stream: "LANDMARKS:hand_landmarks" + output_stream: "NORM_RECT:hand_rect_from_landmarks" + output_stream: "PRESENCE:hand_presence" +} + +# Caches a hand rectangle fed back from HandLandmarkSubgraph, and upon the +# arrival of the next input image sends out the cached rectangle with the +# timestamp replaced by that of the input image, essentially generating a packet +# that carries the previous hand rectangle. Note that upon the arrival of the +# very first input image, an empty packet is sent out to jump start the +# feedback loop. +node { + calculator: "PreviousLoopbackCalculator" + input_stream: "MAIN:input_video" + input_stream: "LOOP:hand_rect_from_landmarks" + input_stream_info: { + tag_index: "LOOP" + back_edge: true + } + output_stream: "PREV_LOOP:prev_hand_rect_from_landmarks" +} + +# Merges a stream of hand rectangles generated by HandDetectionSubgraph and that +# generated by HandLandmarkSubgraph into a single output stream by selecting +# between one of the two streams. The former is selected if the incoming packet +# is not empty, i.e., hand detection is performed on the current image by +# HandDetectionSubgraph (because HandLandmarkSubgraph could not identify hand +# presence in the previous image). Otherwise, the latter is selected, which is +# never empty because HandLandmarkSubgraphs processes all images (that went +# through FlowLimiterCaculator). +node { + calculator: "MergeCalculator" + input_stream: "hand_rect_from_palm_detections" + input_stream: "prev_hand_rect_from_landmarks" + output_stream: "hand_rect" +} + +# Subgraph that renders annotations and overlays them on top of the input +# images (see renderer_cpu.pbtxt). +node { + calculator: "RendererSubgraph" + input_stream: "IMAGE:input_video" + input_stream: "LANDMARKS:hand_landmarks" + input_stream: "NORM_RECT:hand_rect" + input_stream: "DETECTIONS:palm_detections" + output_stream: "IMAGE:output_video" +} + +# Encodes the annotated images into a video file, adopting properties specified +# in the input video header, e.g., video framerate. +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/hand_tracking/hand_tracking_desktop_live.pbtxt b/mediapipe/graphs/hand_tracking/hand_tracking_desktop_live.pbtxt new file mode 100644 index 000000000..3aefbf761 --- /dev/null +++ b/mediapipe/graphs/hand_tracking/hand_tracking_desktop_live.pbtxt @@ -0,0 +1,103 @@ +# MediaPipe graph that performs hand tracking on desktop with TensorFlow Lite +# on CPU. +# Used in the example in +# mediapipie/examples/desktop/hand_tracking:hand_tracking_cpu. + +# Images coming into and out of the graph. +input_stream: "input_video" +output_stream: "output_video" + +# Caches a hand-presence decision fed back from HandLandmarkSubgraph, and upon +# the arrival of the next input image sends out the cached decision with the +# timestamp replaced by that of the input image, essentially generating a packet +# that carries the previous hand-presence decision. Note that upon the arrival +# of the very first input image, an empty packet is sent out to jump start the +# feedback loop. +node { + calculator: "PreviousLoopbackCalculator" + input_stream: "MAIN:input_video" + input_stream: "LOOP:hand_presence" + input_stream_info: { + tag_index: "LOOP" + back_edge: true + } + output_stream: "PREV_LOOP:prev_hand_presence" +} + +# Drops the incoming image if HandLandmarkSubgraph was able to identify hand +# presence in the previous image. Otherwise, passes the incoming image through +# to trigger a new round of hand detection in HandDetectionSubgraph. +node { + calculator: "GateCalculator" + input_stream: "input_video" + input_stream: "DISALLOW:prev_hand_presence" + output_stream: "hand_detection_input_video" + + node_options: { + [type.googleapis.com/mediapipe.GateCalculatorOptions] { + empty_packets_as_allow: true + } + } +} + +# Subgraph that detections hands (see hand_detection_cpu.pbtxt). +node { + calculator: "HandDetectionSubgraph" + input_stream: "hand_detection_input_video" + output_stream: "DETECTIONS:palm_detections" + output_stream: "NORM_RECT:hand_rect_from_palm_detections" +} + +# Subgraph that localizes hand landmarks (see hand_landmark_cpu.pbtxt). +node { + calculator: "HandLandmarkSubgraph" + input_stream: "IMAGE:input_video" + input_stream: "NORM_RECT:hand_rect" + output_stream: "LANDMARKS:hand_landmarks" + output_stream: "NORM_RECT:hand_rect_from_landmarks" + output_stream: "PRESENCE:hand_presence" +} + +# Caches a hand rectangle fed back from HandLandmarkSubgraph, and upon the +# arrival of the next input image sends out the cached rectangle with the +# timestamp replaced by that of the input image, essentially generating a packet +# that carries the previous hand rectangle. Note that upon the arrival of the +# very first input image, an empty packet is sent out to jump start the +# feedback loop. +node { + calculator: "PreviousLoopbackCalculator" + input_stream: "MAIN:input_video" + input_stream: "LOOP:hand_rect_from_landmarks" + input_stream_info: { + tag_index: "LOOP" + back_edge: true + } + output_stream: "PREV_LOOP:prev_hand_rect_from_landmarks" +} + +# Merges a stream of hand rectangles generated by HandDetectionSubgraph and that +# generated by HandLandmarkSubgraph into a single output stream by selecting +# between one of the two streams. The former is selected if the incoming packet +# is not empty, i.e., hand detection is performed on the current image by +# HandDetectionSubgraph (because HandLandmarkSubgraph could not identify hand +# presence in the previous image). Otherwise, the latter is selected, which is +# never empty because HandLandmarkSubgraphs processes all images (that went +# through FlowLimiterCaculator). +node { + calculator: "MergeCalculator" + input_stream: "hand_rect_from_palm_detections" + input_stream: "prev_hand_rect_from_landmarks" + output_stream: "hand_rect" +} + +# Subgraph that renders annotations and overlays them on top of the input +# images (see renderer_cpu.pbtxt). +node { + calculator: "RendererSubgraph" + input_stream: "IMAGE:input_video" + input_stream: "LANDMARKS:hand_landmarks" + input_stream: "NORM_RECT:hand_rect" + input_stream: "DETECTIONS:palm_detections" + output_stream: "IMAGE:output_video" +} + diff --git a/mediapipe/graphs/hand_tracking/subgraphs/BUILD b/mediapipe/graphs/hand_tracking/subgraphs/BUILD new file mode 100644 index 000000000..93a0d1048 --- /dev/null +++ b/mediapipe/graphs/hand_tracking/subgraphs/BUILD @@ -0,0 +1,132 @@ +# 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 + +package(default_visibility = ["//visibility:public"]) + +load( + "//mediapipe/framework/tool:mediapipe_graph.bzl", + "mediapipe_simple_subgraph", +) + +mediapipe_simple_subgraph( + name = "hand_detection_cpu", + graph = "hand_detection_cpu.pbtxt", + register_as = "HandDetectionSubgraph", + deps = [ + "//mediapipe/calculators/image:image_properties_calculator", + "//mediapipe/calculators/image:image_transformation_calculator", + "//mediapipe/calculators/tflite:ssd_anchors_calculator", + "//mediapipe/calculators/tflite:tflite_converter_calculator", + "//mediapipe/calculators/tflite:tflite_custom_op_resolver_calculator", + "//mediapipe/calculators/tflite:tflite_inference_calculator", + "//mediapipe/calculators/tflite:tflite_tensors_to_detections_calculator", + "//mediapipe/calculators/util:detection_label_id_to_text_calculator", + "//mediapipe/calculators/util:detection_letterbox_removal_calculator", + "//mediapipe/calculators/util:detections_to_rects_calculator", + "//mediapipe/calculators/util:detections_to_render_data_calculator", + "//mediapipe/calculators/util:non_max_suppression_calculator", + "//mediapipe/calculators/util:rect_transformation_calculator", + ], +) + +mediapipe_simple_subgraph( + name = "hand_landmark_cpu", + graph = "hand_landmark_cpu.pbtxt", + register_as = "HandLandmarkSubgraph", + deps = [ + "//mediapipe/calculators/core:split_vector_calculator", + "//mediapipe/calculators/image:image_cropping_calculator", + "//mediapipe/calculators/image:image_properties_calculator", + "//mediapipe/calculators/image:image_transformation_calculator", + "//mediapipe/calculators/tflite:tflite_converter_calculator", + "//mediapipe/calculators/tflite:tflite_inference_calculator", + "//mediapipe/calculators/tflite:tflite_tensors_to_floats_calculator", + "//mediapipe/calculators/tflite:tflite_tensors_to_landmarks_calculator", + "//mediapipe/calculators/util:detections_to_rects_calculator", + "//mediapipe/calculators/util:landmark_letterbox_removal_calculator", + "//mediapipe/calculators/util:landmark_projection_calculator", + "//mediapipe/calculators/util:landmarks_to_detection_calculator", + "//mediapipe/calculators/util:landmarks_to_render_data_calculator", + "//mediapipe/calculators/util:rect_transformation_calculator", + "//mediapipe/calculators/util:thresholding_calculator", + ], +) + +mediapipe_simple_subgraph( + name = "renderer_cpu", + graph = "renderer_cpu.pbtxt", + register_as = "RendererSubgraph", + deps = [ + "//mediapipe/calculators/util:annotation_overlay_calculator", + "//mediapipe/calculators/util:detections_to_render_data_calculator", + "//mediapipe/calculators/util:landmarks_to_render_data_calculator", + "//mediapipe/calculators/util:rect_to_render_data_calculator", + ], +) + +mediapipe_simple_subgraph( + name = "hand_detection_gpu", + graph = "hand_detection_gpu.pbtxt", + register_as = "HandDetectionSubgraph", + deps = [ + "//mediapipe/calculators/image:image_properties_calculator", + "//mediapipe/calculators/image:image_transformation_calculator", + "//mediapipe/calculators/tflite:ssd_anchors_calculator", + "//mediapipe/calculators/tflite:tflite_converter_calculator", + "//mediapipe/calculators/tflite:tflite_custom_op_resolver_calculator", + "//mediapipe/calculators/tflite:tflite_inference_calculator", + "//mediapipe/calculators/tflite:tflite_tensors_to_detections_calculator", + "//mediapipe/calculators/util:detection_label_id_to_text_calculator", + "//mediapipe/calculators/util:detection_letterbox_removal_calculator", + "//mediapipe/calculators/util:detections_to_rects_calculator", + "//mediapipe/calculators/util:non_max_suppression_calculator", + "//mediapipe/calculators/util:rect_transformation_calculator", + ], +) + +mediapipe_simple_subgraph( + name = "hand_landmark_gpu", + graph = "hand_landmark_gpu.pbtxt", + register_as = "HandLandmarkSubgraph", + deps = [ + "//mediapipe/calculators/core:split_vector_calculator", + "//mediapipe/calculators/image:image_cropping_calculator", + "//mediapipe/calculators/image:image_properties_calculator", + "//mediapipe/calculators/image:image_transformation_calculator", + "//mediapipe/calculators/tflite:tflite_converter_calculator", + "//mediapipe/calculators/tflite:tflite_inference_calculator", + "//mediapipe/calculators/tflite:tflite_tensors_to_floats_calculator", + "//mediapipe/calculators/tflite:tflite_tensors_to_landmarks_calculator", + "//mediapipe/calculators/util:detections_to_rects_calculator", + "//mediapipe/calculators/util:landmark_letterbox_removal_calculator", + "//mediapipe/calculators/util:landmark_projection_calculator", + "//mediapipe/calculators/util:landmarks_to_detection_calculator", + "//mediapipe/calculators/util:rect_transformation_calculator", + "//mediapipe/calculators/util:thresholding_calculator", + ], +) + +mediapipe_simple_subgraph( + name = "renderer_gpu", + graph = "renderer_gpu.pbtxt", + register_as = "RendererSubgraph", + deps = [ + "//mediapipe/calculators/util:annotation_overlay_calculator", + "//mediapipe/calculators/util:detections_to_render_data_calculator", + "//mediapipe/calculators/util:landmarks_to_render_data_calculator", + "//mediapipe/calculators/util:rect_to_render_data_calculator", + ], +) diff --git a/mediapipe/graphs/hand_tracking/subgraphs/hand_detection_cpu.pbtxt b/mediapipe/graphs/hand_tracking/subgraphs/hand_detection_cpu.pbtxt new file mode 100644 index 000000000..65c7d162f --- /dev/null +++ b/mediapipe/graphs/hand_tracking/subgraphs/hand_detection_cpu.pbtxt @@ -0,0 +1,193 @@ +# MediaPipe hand detection subgraph. + +type: "HandDetectionSubgraph" + +input_stream: "input_video" +output_stream: "DETECTIONS:palm_detections" +output_stream: "NORM_RECT:hand_rect_from_palm_detections" + +# Transforms the input image on CPU to a 256x256 image. To scale the input +# image, the scale_mode option is set to FIT to preserve the aspect ratio, +# resulting in potential letterboxing in the transformed image. +node: { + calculator: "ImageTransformationCalculator" + input_stream: "IMAGE:input_video" + output_stream: "IMAGE:transformed_input_video" + output_stream: "LETTERBOX_PADDING:letterbox_padding" + node_options: { + [type.googleapis.com/mediapipe.ImageTransformationCalculatorOptions] { + output_width: 256 + output_height: 256 + scale_mode: FIT + } + } +} + +# Generates a single side packet containing a TensorFlow Lite op resolver that +# supports custom ops needed by the model used in this graph. +node { + calculator: "TfLiteCustomOpResolverCalculator" + output_side_packet: "op_resolver" +} + +# Converts the transformed input image on CPU into an image tensor as a +# TfLiteTensor. The zero_center option is set to true to normalize the +# pixel values to [-1.f, 1.f] as opposed to [0.f, 1.f]. +node { + calculator: "TfLiteConverterCalculator" + input_stream: "IMAGE:transformed_input_video" + output_stream: "TENSORS:image_tensor" +} + +# Runs a TensorFlow Lite model on CPU that takes an image tensor and outputs a +# vector of tensors representing, for instance, detection boxes/keypoints and +# scores. +node { + calculator: "TfLiteInferenceCalculator" + input_stream: "TENSORS:image_tensor" + output_stream: "TENSORS:detection_tensors" + input_side_packet: "CUSTOM_OP_RESOLVER:op_resolver" + node_options: { + [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] { + model_path: "mediapipe/models/palm_detection.tflite" + } + } +} + +# Generates a single side packet containing a vector of SSD anchors based on +# the specification in the options. +node { + calculator: "SsdAnchorsCalculator" + output_side_packet: "anchors" + node_options: { + [type.googleapis.com/mediapipe.SsdAnchorsCalculatorOptions] { + num_layers: 5 + min_scale: 0.1171875 + max_scale: 0.75 + input_size_height: 256 + input_size_width: 256 + anchor_offset_x: 0.5 + anchor_offset_y: 0.5 + strides: 8 + strides: 16 + strides: 32 + strides: 32 + strides: 32 + aspect_ratios: 1.0 + fixed_anchor_size: true + } + } +} + +# Decodes the detection tensors generated by the TensorFlow Lite model, based on +# the SSD anchors and the specification in the options, into a vector of +# detections. Each detection describes a detected object. +node { + calculator: "TfLiteTensorsToDetectionsCalculator" + input_stream: "TENSORS:detection_tensors" + input_side_packet: "ANCHORS:anchors" + output_stream: "DETECTIONS:detections" + node_options: { + [type.googleapis.com/mediapipe.TfLiteTensorsToDetectionsCalculatorOptions] { + num_classes: 1 + num_boxes: 2944 + num_coords: 18 + box_coord_offset: 0 + keypoint_coord_offset: 4 + num_keypoints: 7 + num_values_per_keypoint: 2 + sigmoid_score: true + score_clipping_thresh: 100.0 + reverse_output_order: true + + x_scale: 256.0 + y_scale: 256.0 + h_scale: 256.0 + w_scale: 256.0 + min_score_thresh: 0.5 + } + } +} + +# Performs non-max suppression to remove excessive detections. +node { + calculator: "NonMaxSuppressionCalculator" + input_stream: "detections" + output_stream: "filtered_detections" + node_options: { + [type.googleapis.com/mediapipe.NonMaxSuppressionCalculatorOptions] { + min_suppression_threshold: 0.3 + min_score_threshold: 0.5 + overlap_type: INTERSECTION_OVER_UNION + algorithm: WEIGHTED + return_empty_detections: true + } + } +} + +# Maps detection label IDs to the corresponding label text. The label map is +# provided in the label_map_path option. +node { + calculator: "DetectionLabelIdToTextCalculator" + input_stream: "filtered_detections" + output_stream: "labeled_detections" + node_options: { + [type.googleapis.com/mediapipe.DetectionLabelIdToTextCalculatorOptions] { + label_map_path: "mediapipe/models/palm_detection_labelmap.txt" + } + } +} + +# Adjusts detection locations (already normalized to [0.f, 1.f]) on the +# letterboxed image (after image transformation with the FIT scale mode) to the +# corresponding locations on the same image with the letterbox removed (the +# input image to the graph before image transformation). +node { + calculator: "DetectionLetterboxRemovalCalculator" + input_stream: "DETECTIONS:labeled_detections" + input_stream: "LETTERBOX_PADDING:letterbox_padding" + output_stream: "DETECTIONS:palm_detections" +} + +# Extracts image size from the input images. +node { + calculator: "ImagePropertiesCalculator" + input_stream: "IMAGE:input_video" + output_stream: "SIZE:image_size" +} + +# Converts results of palm detection into a rectangle (normalized by image size) +# that encloses the palm and is rotated such that the line connecting center of +# the wrist and MCP of the middle finger is aligned with the Y-axis of the +# rectangle. +node { + calculator: "DetectionsToRectsCalculator" + input_stream: "DETECTIONS:palm_detections" + input_stream: "IMAGE_SIZE:image_size" + output_stream: "NORM_RECT:palm_rect" + node_options: { + [type.googleapis.com/mediapipe.DetectionsToRectsCalculatorOptions] { + rotation_vector_start_keypoint_index: 0 # Center of wrist. + rotation_vector_end_keypoint_index: 2 # MCP of middle finger. + rotation_vector_target_angle_degrees: 90 + output_zero_rect_for_empty_detections: true + } + } +} + +# Expands and shifts the rectangle that contains the palm so that it's likely +# to cover the entire hand. +node { + calculator: "RectTransformationCalculator" + input_stream: "NORM_RECT:palm_rect" + input_stream: "IMAGE_SIZE:image_size" + output_stream: "hand_rect_from_palm_detections" + node_options: { + [type.googleapis.com/mediapipe.RectTransformationCalculatorOptions] { + scale_x: 2.6 + scale_y: 2.6 + shift_y: -0.5 + square_long: true + } + } +} diff --git a/mediapipe/graphs/hand_tracking/hand_detection_gpu.pbtxt b/mediapipe/graphs/hand_tracking/subgraphs/hand_detection_gpu.pbtxt similarity index 96% rename from mediapipe/graphs/hand_tracking/hand_detection_gpu.pbtxt rename to mediapipe/graphs/hand_tracking/subgraphs/hand_detection_gpu.pbtxt index 848bacb9f..833286066 100644 --- a/mediapipe/graphs/hand_tracking/hand_detection_gpu.pbtxt +++ b/mediapipe/graphs/hand_tracking/subgraphs/hand_detection_gpu.pbtxt @@ -49,11 +49,11 @@ node { node { calculator: "TfLiteInferenceCalculator" input_stream: "TENSORS_GPU:image_tensor" - output_stream: "TENSORS:detection_tensors" + output_stream: "TENSORS_GPU:detection_tensors" input_side_packet: "CUSTOM_OP_RESOLVER:opresolver" node_options: { [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] { - model_path: "palm_detection.tflite" + model_path: "mediapipe/models/palm_detection.tflite" use_gpu: true } } @@ -89,7 +89,7 @@ node { # detections. Each detection describes a detected object. node { calculator: "TfLiteTensorsToDetectionsCalculator" - input_stream: "TENSORS:detection_tensors" + input_stream: "TENSORS_GPU:detection_tensors" input_side_packet: "ANCHORS:anchors" output_stream: "DETECTIONS:detections" node_options: { @@ -137,7 +137,7 @@ node { output_stream: "labeled_detections" node_options: { [type.googleapis.com/mediapipe.DetectionLabelIdToTextCalculatorOptions] { - label_map_path: "palm_detection_labelmap.txt" + label_map_path: "mediapipe/models/palm_detection_labelmap.txt" } } } diff --git a/mediapipe/graphs/hand_tracking/subgraphs/hand_landmark_cpu.pbtxt b/mediapipe/graphs/hand_tracking/subgraphs/hand_landmark_cpu.pbtxt new file mode 100644 index 000000000..ad52a5716 --- /dev/null +++ b/mediapipe/graphs/hand_tracking/subgraphs/hand_landmark_cpu.pbtxt @@ -0,0 +1,185 @@ +# MediaPipe hand landmark localization subgraph. + +type: "HandLandmarkSubgraph" + +input_stream: "IMAGE:input_video" +input_stream: "NORM_RECT:hand_rect" +output_stream: "LANDMARKS:hand_landmarks" +output_stream: "NORM_RECT:hand_rect_for_next_frame" +output_stream: "PRESENCE:hand_presence" + +# Crops the rectangle that contains a hand from the input image. +node { + calculator: "ImageCroppingCalculator" + input_stream: "IMAGE:input_video" + input_stream: "NORM_RECT:hand_rect" + output_stream: "IMAGE:hand_image" +} + +# Transforms the input image on CPU to a 256x256 image. To scale the input +# image, the scale_mode option is set to FIT to preserve the aspect ratio, +# resulting in potential letterboxing in the transformed image. +node: { + calculator: "ImageTransformationCalculator" + input_stream: "IMAGE:hand_image" + output_stream: "IMAGE:transformed_input_video" + output_stream: "LETTERBOX_PADDING:letterbox_padding" + node_options: { + [type.googleapis.com/mediapipe.ImageTransformationCalculatorOptions] { + output_width: 256 + output_height: 256 + scale_mode: FIT + } + } +} + +# Converts the transformed input image on GPU into an image tensor stored in +# tflite::gpu::GlBuffer. The zero_center option is set to true to normalize the +# pixel values to [-1.f, 1.f] as opposed to [0.f, 1.f]. The flip_vertically +# option is set to true to account for the descrepancy between the +# representation of the input image (origin at the bottom-left corner, the +# OpenGL convention) and what the model used in this graph is expecting (origin +# at the top-left corner). +node { + calculator: "TfLiteConverterCalculator" + input_stream: "IMAGE:transformed_input_video" + output_stream: "TENSORS:image_tensor" + node_options: { + [type.googleapis.com/mediapipe.TfLiteConverterCalculatorOptions] { + zero_center: false + } + } +} + +# Runs a TensorFlow Lite model on GPU that takes an image tensor and outputs a +# vector of tensors representing, for instance, detection boxes/keypoints and +# scores. +node { + calculator: "TfLiteInferenceCalculator" + input_stream: "TENSORS:image_tensor" + output_stream: "TENSORS:output_tensors" + node_options: { + [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] { + model_path: "mediapipe/models/hand_landmark.tflite" + } + } +} + +# Splits a vector of TFLite tensors to multiple vectors according to the ranges +# specified in option. +node { + calculator: "SplitTfLiteTensorVectorCalculator" + input_stream: "output_tensors" + output_stream: "landmark_tensors" + output_stream: "hand_flag_tensor" + node_options: { + [type.googleapis.com/mediapipe.SplitVectorCalculatorOptions] { + ranges: { begin: 0 end: 1 } + ranges: { begin: 1 end: 2 } + } + } +} + +# Converts the hand-flag tensor into a float that represents the confidence +# score of hand presence. +node { + calculator: "TfLiteTensorsToFloatsCalculator" + input_stream: "TENSORS:hand_flag_tensor" + output_stream: "FLOAT:hand_presence_score" +} + +# Applies a threshold to the confidence score to determine whether a hand is +# present. +node { + calculator: "ThresholdingCalculator" + input_stream: "FLOAT:hand_presence_score" + output_stream: "FLAG:hand_presence" + node_options: { + [type.googleapis.com/mediapipe.ThresholdingCalculatorOptions] { + threshold: 0.1 + } + } +} + +# Decodes the landmark tensors into a vector of lanmarks, where the landmark +# coordinates are normalized by the size of the input image to the model. +node { + calculator: "TfLiteTensorsToLandmarksCalculator" + input_stream: "TENSORS:landmark_tensors" + output_stream: "NORM_LANDMARKS:landmarks" + node_options: { + [type.googleapis.com/mediapipe.TfLiteTensorsToLandmarksCalculatorOptions] { + num_landmarks: 21 + input_image_width: 256 + input_image_height: 256 + } + } +} + +# Adjusts landmarks (already normalized to [0.f, 1.f]) on the letterboxed hand +# image (after image transformation with the FIT scale mode) to the +# corresponding locations on the same image with the letterbox removed (hand +# image before image transformation). +node { + calculator: "LandmarkLetterboxRemovalCalculator" + input_stream: "LANDMARKS:landmarks" + input_stream: "LETTERBOX_PADDING:letterbox_padding" + output_stream: "LANDMARKS:scaled_landmarks" +} + +# Projects the landmarks from the cropped hand image to the corresponding +# locations on the full image before cropping (input to the graph). +node { + calculator: "LandmarkProjectionCalculator" + input_stream: "NORM_LANDMARKS:scaled_landmarks" + input_stream: "NORM_RECT:hand_rect" + output_stream: "NORM_LANDMARKS:hand_landmarks" +} + +# Extracts image size from the input images. +node { + calculator: "ImagePropertiesCalculator" + input_stream: "IMAGE:input_video" + output_stream: "SIZE:image_size" +} + +# Converts hand landmarks to a detection that tightly encloses all landmarks. +node { + calculator: "LandmarksToDetectionCalculator" + input_stream: "NORM_LANDMARKS:hand_landmarks" + output_stream: "DETECTION:hand_detection" +} + +# Converts the hand detection into a rectangle (normalized by image size) +# that encloses the hand and is rotated such that the line connecting center of +# the wrist and MCP of the middle finger is aligned with the Y-axis of the +# rectangle. +node { + calculator: "DetectionsToRectsCalculator" + input_stream: "DETECTION:hand_detection" + input_stream: "IMAGE_SIZE:image_size" + output_stream: "NORM_RECT:hand_rect_from_landmarks" + node_options: { + [type.googleapis.com/mediapipe.DetectionsToRectsCalculatorOptions] { + rotation_vector_start_keypoint_index: 0 # Center of wrist. + rotation_vector_end_keypoint_index: 9 # MCP of middle finger. + rotation_vector_target_angle_degrees: 90 + } + } +} + +# Expands the hand rectangle so that in the next video frame it's likely to +# still contain the hand even with some motion. +node { + calculator: "RectTransformationCalculator" + input_stream: "NORM_RECT:hand_rect_from_landmarks" + input_stream: "IMAGE_SIZE:image_size" + output_stream: "hand_rect_for_next_frame" + node_options: { + [type.googleapis.com/mediapipe.RectTransformationCalculatorOptions] { + scale_x: 1.6 + scale_y: 1.6 + square_long: true + } + } +} diff --git a/mediapipe/graphs/hand_tracking/hand_landmark_gpu.pbtxt b/mediapipe/graphs/hand_tracking/subgraphs/hand_landmark_gpu.pbtxt similarity index 96% rename from mediapipe/graphs/hand_tracking/hand_landmark_gpu.pbtxt rename to mediapipe/graphs/hand_tracking/subgraphs/hand_landmark_gpu.pbtxt index 467abd4c5..283ce459c 100644 --- a/mediapipe/graphs/hand_tracking/hand_landmark_gpu.pbtxt +++ b/mediapipe/graphs/hand_tracking/subgraphs/hand_landmark_gpu.pbtxt @@ -39,6 +39,11 @@ node { calculator: "TfLiteConverterCalculator" input_stream: "IMAGE_GPU:transformed_hand_image" output_stream: "TENSORS_GPU:image_tensor" + node_options: { + [type.googleapis.com/mediapipe.TfLiteConverterCalculatorOptions] { + zero_center: false + } + } } # Runs a TensorFlow Lite model on GPU that takes an image tensor and outputs a @@ -50,7 +55,7 @@ node { output_stream: "TENSORS:output_tensors" node_options: { [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] { - model_path: "hand_landmark.tflite" + model_path: "mediapipe/models/hand_landmark.tflite" use_gpu: true } } diff --git a/mediapipe/graphs/hand_tracking/subgraphs/renderer_cpu.pbtxt b/mediapipe/graphs/hand_tracking/subgraphs/renderer_cpu.pbtxt new file mode 100644 index 000000000..c3033155d --- /dev/null +++ b/mediapipe/graphs/hand_tracking/subgraphs/renderer_cpu.pbtxt @@ -0,0 +1,102 @@ +# MediaPipe hand tracking rendering subgraph. + +type: "RendererSubgraph" + +input_stream: "IMAGE:input_image" +input_stream: "DETECTIONS:detections" +input_stream: "LANDMARKS:landmarks" +input_stream: "NORM_RECT:rect" +output_stream: "IMAGE:output_image" + +# Converts detections to drawing primitives for annotation overlay. +node { + calculator: "DetectionsToRenderDataCalculator" + input_stream: "DETECTIONS:detections" + output_stream: "RENDER_DATA:detection_render_data" + node_options: { + [type.googleapis.com/mediapipe.DetectionsToRenderDataCalculatorOptions] { + thickness: 4.0 + color { r: 0 g: 255 b: 0 } + } + } +} + +# Converts landmarks to drawing primitives for annotation overlay. +node { + calculator: "LandmarksToRenderDataCalculator" + input_stream: "NORM_LANDMARKS:landmarks" + output_stream: "RENDER_DATA:landmark_render_data" + node_options: { + [type.googleapis.com/mediapipe.LandmarksToRenderDataCalculatorOptions] { + landmark_connections: 0 + landmark_connections: 1 + landmark_connections: 1 + landmark_connections: 2 + landmark_connections: 2 + landmark_connections: 3 + landmark_connections: 3 + landmark_connections: 4 + landmark_connections: 0 + landmark_connections: 5 + landmark_connections: 5 + landmark_connections: 6 + landmark_connections: 6 + landmark_connections: 7 + landmark_connections: 7 + landmark_connections: 8 + landmark_connections: 5 + landmark_connections: 9 + landmark_connections: 9 + landmark_connections: 10 + landmark_connections: 10 + landmark_connections: 11 + landmark_connections: 11 + landmark_connections: 12 + landmark_connections: 9 + landmark_connections: 13 + landmark_connections: 13 + landmark_connections: 14 + landmark_connections: 14 + landmark_connections: 15 + landmark_connections: 15 + landmark_connections: 16 + landmark_connections: 13 + landmark_connections: 17 + landmark_connections: 0 + landmark_connections: 17 + landmark_connections: 17 + landmark_connections: 18 + landmark_connections: 18 + landmark_connections: 19 + landmark_connections: 19 + landmark_connections: 20 + landmark_color { r: 255 g: 0 b: 0 } + connection_color { r: 0 g: 255 b: 0 } + thickness: 4.0 + } + } +} + +# Converts normalized rects to drawing primitives for annotation overlay. +node { + calculator: "RectToRenderDataCalculator" + input_stream: "NORM_RECT:rect" + output_stream: "RENDER_DATA:rect_render_data" + node_options: { + [type.googleapis.com/mediapipe.RectToRenderDataCalculatorOptions] { + filled: false + color { r: 255 g: 0 b: 0 } + thickness: 4.0 + } + } +} + +# Draws annotations and overlays them on top of the input images. +node { + calculator: "AnnotationOverlayCalculator" + input_stream: "INPUT_FRAME:input_image" + input_stream: "detection_render_data" + input_stream: "landmark_render_data" + input_stream: "rect_render_data" + output_stream: "OUTPUT_FRAME:output_image" +} diff --git a/mediapipe/graphs/hand_tracking/renderer_gpu.pbtxt b/mediapipe/graphs/hand_tracking/subgraphs/renderer_gpu.pbtxt similarity index 100% rename from mediapipe/graphs/hand_tracking/renderer_gpu.pbtxt rename to mediapipe/graphs/hand_tracking/subgraphs/renderer_gpu.pbtxt diff --git a/mediapipe/graphs/object_detection/BUILD b/mediapipe/graphs/object_detection/BUILD index cd1d1b6be..36c0181a9 100644 --- a/mediapipe/graphs/object_detection/BUILD +++ b/mediapipe/graphs/object_detection/BUILD @@ -56,6 +56,10 @@ cc_library( cc_library( name = "desktop_tflite_calculators", deps = [ + "//mediapipe/calculators/core:concatenate_vector_calculator", + "//mediapipe/calculators/core:flow_limiter_calculator", + "//mediapipe/calculators/core:previous_loopback_calculator", + "//mediapipe/calculators/core:split_vector_calculator", "//mediapipe/calculators/image:image_transformation_calculator", "//mediapipe/calculators/tflite:ssd_anchors_calculator", "//mediapipe/calculators/tflite:tflite_converter_calculator", diff --git a/mediapipe/graphs/object_detection/object_detection_desktop_live.pbtxt b/mediapipe/graphs/object_detection/object_detection_desktop_live.pbtxt new file mode 100644 index 000000000..899785a1c --- /dev/null +++ b/mediapipe/graphs/object_detection/object_detection_desktop_live.pbtxt @@ -0,0 +1,174 @@ +# MediaPipe graph that performs object detection with TensorFlow Lite on CPU. +# Used in the examples in +# mediapipie/examples/desktop/object_detection:object_detection_cpu. + +# Images on CPU coming into and out of the graph. +input_stream: "input_video" +output_stream: "output_video" + +# Throttles the images flowing downstream for flow control. It passes through +# the very first incoming image unaltered, and waits for +# TfLiteTensorsToDetectionsCalculator downstream in the graph to finish +# generating the corresponding detections before it passes through another +# image. All images that come in while waiting are dropped, limiting the number +# of in-flight images between this calculator and +# TfLiteTensorsToDetectionsCalculator to 1. This prevents the nodes in between +# from queuing up incoming images and data excessively, which leads to increased +# latency and memory usage, unwanted in real-time mobile applications. It also +# eliminates unnecessarily computation, e.g., a transformed image produced by +# ImageTransformationCalculator may get dropped downstream if the subsequent +# TfLiteConverterCalculator or TfLiteInferenceCalculator is still busy +# processing previous inputs. +node { + calculator: "FlowLimiterCalculator" + input_stream: "input_video" + input_stream: "FINISHED:detections" + input_stream_info: { + tag_index: "FINISHED" + back_edge: true + } + output_stream: "throttled_input_video" +} + +# Transforms the input image on CPU to a 320x320 image. To scale the image, by +# default it uses the STRETCH scale mode that maps the entire input image to the +# entire transformed image. As a result, image aspect ratio may be changed and +# objects in the image may be deformed (stretched or squeezed), but the object +# detection model used in this graph is agnostic to that deformation. +node: { + calculator: "ImageTransformationCalculator" + input_stream: "IMAGE:throttled_input_video" + output_stream: "IMAGE:transformed_input_video" + node_options: { + [type.googleapis.com/mediapipe.ImageTransformationCalculatorOptions] { + output_width: 320 + output_height: 320 + } + } +} + +# Converts the transformed input image on CPU into an image tensor stored as a +# TfLiteTensor. +node { + calculator: "TfLiteConverterCalculator" + input_stream: "IMAGE:transformed_input_video" + output_stream: "TENSORS:image_tensor" +} + +# Runs a TensorFlow Lite model on CPU that takes an image tensor and outputs a +# vector of tensors representing, for instance, detection boxes/keypoints and +# scores. +node { + calculator: "TfLiteInferenceCalculator" + input_stream: "TENSORS:image_tensor" + output_stream: "TENSORS:detection_tensors" + node_options: { + [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] { + model_path: "mediapipe/models/ssdlite_object_detection.tflite" + } + } +} + +# Generates a single side packet containing a vector of SSD anchors based on +# the specification in the options. +node { + calculator: "SsdAnchorsCalculator" + output_side_packet: "anchors" + node_options: { + [type.googleapis.com/mediapipe.SsdAnchorsCalculatorOptions] { + num_layers: 6 + min_scale: 0.2 + max_scale: 0.95 + input_size_height: 320 + input_size_width: 320 + anchor_offset_x: 0.5 + anchor_offset_y: 0.5 + strides: 16 + strides: 32 + strides: 64 + strides: 128 + strides: 256 + strides: 512 + aspect_ratios: 1.0 + aspect_ratios: 2.0 + aspect_ratios: 0.5 + aspect_ratios: 3.0 + aspect_ratios: 0.3333 + reduce_boxes_in_lowest_layer: true + } + } +} + +# Decodes the detection tensors generated by the TensorFlow Lite model, based on +# the SSD anchors and the specification in the options, into a vector of +# detections. Each detection describes a detected object. +node { + calculator: "TfLiteTensorsToDetectionsCalculator" + input_stream: "TENSORS:detection_tensors" + input_side_packet: "ANCHORS:anchors" + output_stream: "DETECTIONS:detections" + node_options: { + [type.googleapis.com/mediapipe.TfLiteTensorsToDetectionsCalculatorOptions] { + num_classes: 91 + num_boxes: 2034 + num_coords: 4 + ignore_classes: 0 + sigmoid_score: true + apply_exponential_on_box_size: true + x_scale: 10.0 + y_scale: 10.0 + h_scale: 5.0 + w_scale: 5.0 + min_score_thresh: 0.6 + } + } +} + +# Performs non-max suppression to remove excessive detections. +node { + calculator: "NonMaxSuppressionCalculator" + input_stream: "detections" + output_stream: "filtered_detections" + node_options: { + [type.googleapis.com/mediapipe.NonMaxSuppressionCalculatorOptions] { + min_suppression_threshold: 0.4 + max_num_detections: 3 + overlap_type: INTERSECTION_OVER_UNION + return_empty_detections: true + } + } +} + +# Maps detection label IDs to the corresponding label text. The label map is +# provided in the label_map_path option. +node { + calculator: "DetectionLabelIdToTextCalculator" + input_stream: "filtered_detections" + output_stream: "output_detections" + node_options: { + [type.googleapis.com/mediapipe.DetectionLabelIdToTextCalculatorOptions] { + label_map_path: "mediapipe/models/ssdlite_object_detection_labelmap.txt" + } + } +} + +# Converts the detections to drawing primitives for annotation overlay. +node { + calculator: "DetectionsToRenderDataCalculator" + input_stream: "DETECTIONS:output_detections" + output_stream: "RENDER_DATA:render_data" + node_options: { + [type.googleapis.com/mediapipe.DetectionsToRenderDataCalculatorOptions] { + thickness: 4.0 + color { r: 255 g: 0 b: 0 } + } + } +} + +# Draws annotations and overlays them on top of the input images. +node { + calculator: "AnnotationOverlayCalculator" + input_stream: "INPUT_FRAME:throttled_input_video" + input_stream: "render_data" + output_stream: "OUTPUT_FRAME:output_video" +} diff --git a/mediapipe/graphs/object_detection/object_detection_mobile_cpu.pbtxt b/mediapipe/graphs/object_detection/object_detection_mobile_cpu.pbtxt index 4eb527a3c..3e0e4e6d3 100644 --- a/mediapipe/graphs/object_detection/object_detection_mobile_cpu.pbtxt +++ b/mediapipe/graphs/object_detection/object_detection_mobile_cpu.pbtxt @@ -75,7 +75,7 @@ node { output_stream: "TENSORS:detection_tensors" node_options: { [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] { - model_path: "ssdlite_object_detection.tflite" + model_path: "mediapipe/models/ssdlite_object_detection.tflite" } } } @@ -158,7 +158,7 @@ node { output_stream: "output_detections" node_options: { [type.googleapis.com/mediapipe.DetectionLabelIdToTextCalculatorOptions] { - label_map_path: "ssdlite_object_detection_labelmap.txt" + label_map_path: "mediapipe/models/ssdlite_object_detection_labelmap.txt" } } } diff --git a/mediapipe/graphs/object_detection/object_detection_mobile_gpu.pbtxt b/mediapipe/graphs/object_detection/object_detection_mobile_gpu.pbtxt index 44bf61057..dfed16696 100644 --- a/mediapipe/graphs/object_detection/object_detection_mobile_gpu.pbtxt +++ b/mediapipe/graphs/object_detection/object_detection_mobile_gpu.pbtxt @@ -62,10 +62,10 @@ node { node { calculator: "TfLiteInferenceCalculator" input_stream: "TENSORS_GPU:image_tensor" - output_stream: "TENSORS:detection_tensors" + output_stream: "TENSORS_GPU:detection_tensors" node_options: { [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] { - model_path: "ssdlite_object_detection.tflite" + model_path: "mediapipe/models/ssdlite_object_detection.tflite" } } } @@ -105,7 +105,7 @@ node { # detections. Each detection describes a detected object. node { calculator: "TfLiteTensorsToDetectionsCalculator" - input_stream: "TENSORS:detection_tensors" + input_stream: "TENSORS_GPU:detection_tensors" input_side_packet: "ANCHORS:anchors" output_stream: "DETECTIONS:detections" node_options: { @@ -148,7 +148,7 @@ node { output_stream: "output_detections" node_options: { [type.googleapis.com/mediapipe.DetectionLabelIdToTextCalculatorOptions] { - label_map_path: "ssdlite_object_detection_labelmap.txt" + label_map_path: "mediapipe/models/ssdlite_object_detection_labelmap.txt" } } } diff --git a/mediapipe/graphs/youtube8m/BUILD b/mediapipe/graphs/youtube8m/BUILD index 4bfb0d46d..be0fff44c 100644 --- a/mediapipe/graphs/youtube8m/BUILD +++ b/mediapipe/graphs/youtube8m/BUILD @@ -17,7 +17,7 @@ licenses(["notice"]) # Apache 2.0 package(default_visibility = ["//visibility:public"]) cc_library( - name = "yt8m_calculators_deps", + name = "yt8m_feature_extraction_calculators", deps = [ "//mediapipe/calculators/audio:audio_decoder_calculator", "//mediapipe/calculators/audio:basic_time_series_calculators", diff --git a/mediapipe/graphs/youtube8m/feature_extraction.pbtxt b/mediapipe/graphs/youtube8m/feature_extraction.pbtxt index 42fd5988e..89d1053de 100644 --- a/mediapipe/graphs/youtube8m/feature_extraction.pbtxt +++ b/mediapipe/graphs/youtube8m/feature_extraction.pbtxt @@ -16,12 +16,16 @@ node { input_side_packet: "SEQUENCE_EXAMPLE:parsed_sequence_example" output_side_packet: "DATA_PATH:input_file" output_side_packet: "RESAMPLER_OPTIONS:packet_resampler_options" + output_side_packet: "AUDIO_DECODER_OPTIONS:audio_decoder_options" node_options: { [type.googleapis.com/mediapipe.UnpackMediaSequenceCalculatorOptions]: { base_packet_resampler_options { frame_rate: 1.0 base_timestamp: 0 } + base_audio_decoder_options { + audio_stream { stream_index: 0 } + } } } } @@ -121,13 +125,9 @@ node { node { calculator: "AudioDecoderCalculator" input_side_packet: "INPUT_FILE_PATH:input_file" + input_side_packet: "OPTIONS:audio_decoder_options" output_stream: "AUDIO:audio" output_stream: "AUDIO_HEADER:audio_header" - node_options: { - [type.googleapis.com/mediapipe.AudioDecoderOptions]: { - audio_stream { stream_index: 0 } - } - } } node { diff --git a/mediapipe/java/com/google/mediapipe/components/FrameProcessor.java b/mediapipe/java/com/google/mediapipe/components/FrameProcessor.java index ba506524f..c63f0495a 100644 --- a/mediapipe/java/com/google/mediapipe/components/FrameProcessor.java +++ b/mediapipe/java/com/google/mediapipe/components/FrameProcessor.java @@ -185,16 +185,10 @@ public class FrameProcessor implements TextureFrameProcessor { public void close() { if (started.get()) { try { - mediapipeGraph.closeAllInputStreams(); - - // TODO Add a way to signal a source calculator to stop. - // Required for a graph containing a source calculator to shut down properly. - mediapipeGraph.cancelGraph(); - + mediapipeGraph.closeAllPacketSources(); mediapipeGraph.waitUntilGraphDone(); } catch (MediaPipeException e) { - // TODO: cancelGraph will cause an exception to be raised in waitUntilGraphDone. - // We should not cancel the graph here! Also, we should handle exceptions better. + Log.e(TAG, "Mediapipe error: ", e); } try { mediapipeGraph.tearDown(); diff --git a/mediapipe/java/com/google/mediapipe/framework/Graph.java b/mediapipe/java/com/google/mediapipe/framework/Graph.java index 0feabf386..9065d4e50 100644 --- a/mediapipe/java/com/google/mediapipe/framework/Graph.java +++ b/mediapipe/java/com/google/mediapipe/framework/Graph.java @@ -16,7 +16,6 @@ package com.google.mediapipe.framework; import com.google.common.base.Preconditions; import com.google.common.flogger.FluentLogger; -import com.google.mediapipe.proto.CalculatorOptionsProto.CalculatorOptions; import com.google.mediapipe.proto.CalculatorProto.CalculatorGraphConfig; import com.google.mediapipe.proto.GraphTemplateProto.CalculatorGraphTemplate; import com.google.protobuf.InvalidProtocolBufferException; @@ -119,7 +118,7 @@ public class Graph { } /** Specifies options such as template arguments for the graph. */ - public synchronized void setGraphOptions(CalculatorOptions options) { + public synchronized void setGraphOptions(CalculatorGraphConfig.Node options) { nativeSetGraphOptions(nativeGraphHandle, options.toByteArray()); } diff --git a/mediapipe/java/com/google/mediapipe/framework/jni/BUILD b/mediapipe/java/com/google/mediapipe/framework/jni/BUILD index a02cd2e33..182226cbb 100644 --- a/mediapipe/java/com/google/mediapipe/framework/jni/BUILD +++ b/mediapipe/java/com/google/mediapipe/framework/jni/BUILD @@ -73,9 +73,6 @@ cc_library( ], "//mediapipe/gpu:disable_gpu": [], }), - copts = [ - "-DDISABLE_GOOGLE_GLOBAL_USING_DECLARATIONS", # b/33667913 - ], linkopts = select({ "//conditions:default": [], "//mediapipe:android": [ @@ -123,7 +120,9 @@ cc_library( "//mediapipe/gpu:gpu_shared_data_internal", "//mediapipe/gpu:graph_support", ], - "//mediapipe/gpu:disable_gpu": [], + "//mediapipe/gpu:disable_gpu": [ + "//mediapipe/gpu:gpu_shared_data_internal", + ], }), alwayslink = 1, ) diff --git a/mediapipe/java/com/google/mediapipe/framework/jni/graph.h b/mediapipe/java/com/google/mediapipe/framework/jni/graph.h index 39bd91446..c6f64b6fe 100644 --- a/mediapipe/java/com/google/mediapipe/framework/jni/graph.h +++ b/mediapipe/java/com/google/mediapipe/framework/jni/graph.h @@ -191,7 +191,7 @@ class Graph { // CalculatorGraphTemplates for the calculator graph and subgraphs. std::vector graph_templates_; // Options such as template arguments for the top-level calculator graph. - CalculatorOptions graph_options_; + Subgraph::SubgraphOptions graph_options_; // The CalculatorGraphConfig::type of the top-level calculator graph. std::string graph_type_ = ""; diff --git a/mediapipe/objc/MPPGraph.h b/mediapipe/objc/MPPGraph.h index e0371c081..6823aad18 100644 --- a/mediapipe/objc/MPPGraph.h +++ b/mediapipe/objc/MPPGraph.h @@ -116,6 +116,13 @@ typedef NS_ENUM(int, MPPPacketType) { /// @param name The name of the input side packet. - (void)setSidePacket:(const mediapipe::Packet&)packet named:(const std::string&)name; +/// Sets a service packet. If it was already set, it is overwritten. +/// Must be called before the graph is started. +/// @param packet The packet to be associated with the service. +/// @param service. +- (void)setServicePacket:(mediapipe::Packet&)packet + forService:(const mediapipe::GraphServiceBase&)service; + /// Adds input side packets from a map. Any inputs that were already set are /// left unchanged. /// Must be called before the graph is started. diff --git a/mediapipe/objc/MPPGraph.mm b/mediapipe/objc/MPPGraph.mm index 636f3edbf..8c9da2011 100644 --- a/mediapipe/objc/MPPGraph.mm +++ b/mediapipe/objc/MPPGraph.mm @@ -22,6 +22,7 @@ #include "absl/memory/memory.h" #include "mediapipe/framework/calculator_framework.h" #include "mediapipe/framework/formats/image_frame.h" +#include "mediapipe/framework/graph_service.h" #include "mediapipe/gpu/MPPGraphGPUData.h" #include "mediapipe/gpu/gl_base.h" #include "mediapipe/gpu/gpu_shared_data_internal.h" @@ -38,6 +39,8 @@ std::map _inputSidePackets; /// Packet headers that will be added to the graph when it is started. std::map _streamHeaders; + /// Service packets to be added to the graph when it is started. + std::map _servicePackets; /// Number of frames currently being processed by the graph. std::atomic _framesInFlight; @@ -199,6 +202,13 @@ void CallFrameDelegate(void* wrapperVoid, const std::string& streamName, _inputSidePackets[name] = packet; } +- (void)setServicePacket:(mediapipe::Packet&)packet + forService:(const mediapipe::GraphServiceBase&)service { + _GTMDevAssert(!_started, @"%@ must be called before the graph is started", + NSStringFromSelector(_cmd)); + _servicePackets[&service] = std::move(packet); +} + - (void)addSidePackets:(const std::map&)extraSidePackets { _GTMDevAssert(!_started, @"%@ must be called before the graph is started", NSStringFromSelector(_cmd)); @@ -206,18 +216,33 @@ void CallFrameDelegate(void* wrapperVoid, const std::string& streamName, } - (BOOL)startWithError:(NSError**)error { + ::mediapipe::Status status = [self performStart]; + if (!status.ok()) { + if (error) { + *error = [NSError gus_errorWithStatus:status]; + } + return NO; + } + _started = YES; + return YES; +} + +- (::mediapipe::Status)performStart { ::mediapipe::Status status = _graph->Initialize(_config); - if (status.ok()) { - status = _graph->StartRun(_inputSidePackets, _streamHeaders); - if (status.ok()) { - _started = YES; - return YES; + if (!status.ok()) { + return status; + } + for (const auto& service_packet : _servicePackets) { + status = _graph->SetServicePacket(*service_packet.first, service_packet.second); + if (!status.ok()) { + return status; } } - if (error) { - *error = [NSError gus_errorWithStatus:status]; + status = _graph->StartRun(_inputSidePackets, _streamHeaders); + if (!status.ok()) { + return status; } - return NO; + return status; } - (void)cancel { diff --git a/mediapipe/objc/MPPGraphTestBase.h b/mediapipe/objc/MPPGraphTestBase.h index c9b67c264..7c457fbbe 100644 --- a/mediapipe/objc/MPPGraphTestBase.h +++ b/mediapipe/objc/MPPGraphTestBase.h @@ -61,6 +61,9 @@ /// Loads an image from the test bundle. - (UIImage*)testImageNamed:(NSString*)name extension:(NSString*)extension; +/// Returns a URL for a file.extension in the test bundle. +- (NSURL*)URLForTestFile:(NSString*)file extension:(NSString*)extension; + /// Loads an image from the test bundle in subpath. - (UIImage*)testImageNamed:(NSString*)name extension:(NSString*)extension diff --git a/mediapipe/objc/MPPGraphTestBase.mm b/mediapipe/objc/MPPGraphTestBase.mm index cfb757369..46fe42755 100644 --- a/mediapipe/objc/MPPGraphTestBase.mm +++ b/mediapipe/objc/MPPGraphTestBase.mm @@ -43,9 +43,13 @@ static void EnsureOutputDirFor(NSString *outputFile) { @implementation MPPGraphTestBase -- (NSData*)testDataNamed:(NSString*)name extension:(NSString*)extension { +- (NSURL*)URLForTestFile:(NSString*)file extension:(NSString*)extension { NSBundle* testBundle = [NSBundle bundleForClass:[self class]]; - NSURL* resourceURL = [testBundle URLForResource:name withExtension:extension]; + return [testBundle URLForResource:file withExtension:extension]; +} + +- (NSData*)testDataNamed:(NSString*)name extension:(NSString*)extension { + NSURL* resourceURL = [self URLForTestFile:name extension:extension]; XCTAssertNotNil(resourceURL, @"Unable to find data with name: %@. Did you add it to your resources?", name); NSError* error; diff --git a/mediapipe/util/android/file/base/helpers.cc b/mediapipe/util/android/file/base/helpers.cc index bfa144d9a..930f916fa 100644 --- a/mediapipe/util/android/file/base/helpers.cc +++ b/mediapipe/util/android/file/base/helpers.cc @@ -47,8 +47,9 @@ class FdCloser { const file::Options& /*options*/) { int fd = open(std::string(file_name).c_str(), O_RDONLY); if (fd < 0) { - return ::mediapipe::Status(mediapipe::StatusCode::kUnknown, - "Failed to open file"); + return ::mediapipe::Status( + mediapipe::StatusCode::kUnknown, + "Failed to open file: " + std::string(file_name)); } FdCloser closer(fd); @@ -92,8 +93,9 @@ class FdCloser { int fd = open(std::string(file_name).c_str(), O_WRONLY | O_CREAT | O_TRUNC, mode); if (fd < 0) { - return ::mediapipe::Status(mediapipe::StatusCode::kUnknown, - "Failed to open file"); + return ::mediapipe::Status( + mediapipe::StatusCode::kUnknown, + "Failed to open file: " + std::string(file_name)); } int bytes_written = 0; diff --git a/mediapipe/util/annotation_renderer.cc b/mediapipe/util/annotation_renderer.cc index 85ca2e6b7..54ba7bb17 100644 --- a/mediapipe/util/annotation_renderer.cc +++ b/mediapipe/util/annotation_renderer.cc @@ -357,7 +357,8 @@ void AnnotationRenderer::DrawFilledOval(const RenderAnnotation& annotation) { bottom = static_cast(enclosing_rectangle.bottom()); } cv::Point center((left + right) / 2, (top + bottom) / 2); - cv::Size size((right - left) / 2, (bottom - top) / 2); + cv::Size size(std::max(0, (right - left) / 2), + std::max(0, (bottom - top) / 2)); const cv::Scalar color = MediapipeColorToOpenCVColor(annotation.color()); cv::ellipse(mat_image_, center, size, 0, 0, 360, color, -1); } diff --git a/mediapipe/util/resource_util_android.cc b/mediapipe/util/resource_util_android.cc index 00d3b83f6..643739566 100644 --- a/mediapipe/util/resource_util_android.cc +++ b/mediapipe/util/resource_util_android.cc @@ -21,13 +21,38 @@ namespace mediapipe { +namespace { +::mediapipe::StatusOr PathToResourceAsFileInternal( + const std::string& path) { + return Singleton::get()->CachedFileFromAsset(path); +} +} // namespace + ::mediapipe::StatusOr PathToResourceAsFile( const std::string& path) { + // Return full path. if (absl::StartsWith(path, "/")) { return path; } - return Singleton::get()->CachedFileFromAsset(path); + // Try to load a relative path or a base filename as is. + { + auto status_or_path = PathToResourceAsFileInternal(path); + if (status_or_path.ok()) { + LOG(INFO) << "Successfully loaded: " << path; + return status_or_path; + } + } + + // If that fails, assume it was a relative path, and try just the base name. + { + const size_t last_slash_idx = path.find_last_of("\\/"); + CHECK_NE(last_slash_idx, std::string::npos); // Make sure it's a path. + auto base_name = path.substr(last_slash_idx + 1); + auto status_or_path = PathToResourceAsFileInternal(base_name); + if (status_or_path.ok()) LOG(INFO) << "Successfully loaded: " << base_name; + return status_or_path; + } } ::mediapipe::Status GetResourceContents(const std::string& path, diff --git a/mediapipe/util/resource_util_apple.cc b/mediapipe/util/resource_util_apple.cc index bcd90fe1a..9b7677679 100644 --- a/mediapipe/util/resource_util_apple.cc +++ b/mediapipe/util/resource_util_apple.cc @@ -23,25 +23,48 @@ namespace mediapipe { -::mediapipe::StatusOr PathToResourceAsFile( +namespace { +::mediapipe::StatusOr PathToResourceAsFileInternal( const std::string& path) { - if (absl::StartsWith(path, "/")) { - return path; - } - NSString* ns_path = [NSString stringWithUTF8String:path.c_str()]; Class mediapipeGraphClass = NSClassFromString(@"MPPGraph"); NSString* resource_dir = [[NSBundle bundleForClass:mediapipeGraphClass] resourcePath]; NSString* resolved_ns_path = [resource_dir stringByAppendingPathComponent:ns_path]; - std::string resolved_path = [resolved_ns_path UTF8String]; RET_CHECK([[NSFileManager defaultManager] fileExistsAtPath:resolved_ns_path]) << "cannot find file: " << resolved_path; - return resolved_path; } +} // namespace + +::mediapipe::StatusOr PathToResourceAsFile( + const std::string& path) { + // Return full path. + if (absl::StartsWith(path, "/")) { + return path; + } + + // Try to load a relative path or a base filename as is. + { + auto status_or_path = PathToResourceAsFileInternal(path); + if (status_or_path.ok()) { + LOG(INFO) << "Successfully loaded: " << path; + return status_or_path; + } + } + + // If that fails, assume it was a relative path, and try just the base name. + { + const size_t last_slash_idx = path.find_last_of("\\/"); + CHECK_NE(last_slash_idx, std::string::npos); // Make sure it's a path. + auto base_name = path.substr(last_slash_idx + 1); + auto status_or_path = PathToResourceAsFileInternal(base_name); + if (status_or_path.ok()) LOG(INFO) << "Successfully loaded: " << base_name; + return status_or_path; + } +} ::mediapipe::Status GetResourceContents(const std::string& path, std::string* output) { diff --git a/third_party/com_github_glog_glog_9779e5ea6ef59562b030248947f787d1256132ae.diff b/third_party/com_github_glog_glog_9779e5ea6ef59562b030248947f787d1256132ae.diff new file mode 100644 index 000000000..776e6d671 --- /dev/null +++ b/third_party/com_github_glog_glog_9779e5ea6ef59562b030248947f787d1256132ae.diff @@ -0,0 +1,58 @@ +commit 9779e5ea6ef59562b030248947f787d1256132ae +Author: jqtang +Date: Wed Sep 18 11:43:48 2019 -0700 + + Add glog Android support for MediaPipe. + +diff --git a/src/logging.cc b/src/logging.cc +index 0b5e6ee..be5a506 100644 +--- a/src/logging.cc ++++ b/src/logging.cc +@@ -67,6 +67,10 @@ + # include "stacktrace.h" + #endif + ++#ifdef __ANDROID__ ++#include ++#endif ++ + using std::string; + using std::vector; + using std::setw; +@@ -1279,6 +1283,23 @@ ostream& LogMessage::stream() { + return data_->stream_; + } + ++namespace { ++#if defined(__ANDROID__) ++int AndroidLogLevel(const int severity) { ++ switch (severity) { ++ case 3: ++ return ANDROID_LOG_FATAL; ++ case 2: ++ return ANDROID_LOG_ERROR; ++ case 1: ++ return ANDROID_LOG_WARN; ++ default: ++ return ANDROID_LOG_INFO; ++ } ++} ++#endif // defined(__ANDROID__) ++} // namespace ++ + // Flush buffered message, called by the destructor, or any other function + // that needs to synchronize the log. + void LogMessage::Flush() { +@@ -1313,6 +1334,12 @@ void LogMessage::Flush() { + } + LogDestination::WaitForSinks(data_); + ++#if defined(__ANDROID__) ++ const int level = AndroidLogLevel((int)data_->severity_); ++ const std::string text = std::string(data_->message_text_); ++ __android_log_write(level, "native", text.substr(0,data_->num_chars_to_log_).c_str()); ++#endif // !defined(__ANDROID__) ++ + if (append_newline) { + // Fix the ostrstream back how it was before we screwed with it. + // It's 99.44% certain that we don't need to worry about doing this. \ No newline at end of file