Project import generated by Copybara.
GitOrigin-RevId: 373e3ac1e5839befd95bf7d73ceff3c5f1171969
This commit is contained in:
@@ -30,7 +30,7 @@ message FlowLimiterCalculatorOptions {
|
||||
optional int32 max_in_flight = 1 [default = 1];
|
||||
|
||||
// The maximum number of frames queued waiting for processing.
|
||||
// The default value limits to 1 frame awaiting processing.
|
||||
// The default value limits to 0 frames awaiting processing.
|
||||
optional int32 max_in_queue = 2 [default = 0];
|
||||
|
||||
// The maximum time in microseconds to wait for a frame to finish processing.
|
||||
|
||||
@@ -80,4 +80,7 @@ typedef SplitVectorCalculator<mediapipe::ClassificationList, false>
|
||||
SplitClassificationListVectorCalculator;
|
||||
REGISTER_CALCULATOR(SplitClassificationListVectorCalculator);
|
||||
|
||||
typedef SplitVectorCalculator<uint64_t, false> SplitUint64tVectorCalculator;
|
||||
REGISTER_CALCULATOR(SplitUint64tVectorCalculator);
|
||||
|
||||
} // namespace mediapipe
|
||||
|
||||
@@ -480,8 +480,7 @@ RectSpec ImageCroppingCalculator::GetCropSpecs(const CalculatorContext* cc,
|
||||
if (cc->Inputs().HasTag(kRectTag)) {
|
||||
const auto& rect = cc->Inputs().Tag(kRectTag).Get<Rect>();
|
||||
// Only use the rect if it is valid.
|
||||
if (rect.width() > 0 && rect.height() > 0 && rect.x_center() >= 0 &&
|
||||
rect.y_center() >= 0) {
|
||||
if (rect.width() > 0 && rect.height() > 0) {
|
||||
x_center = rect.x_center();
|
||||
y_center = rect.y_center();
|
||||
crop_width = rect.width();
|
||||
|
||||
@@ -337,12 +337,15 @@ absl::Status ImageTransformationCalculator::Process(CalculatorContext* cc) {
|
||||
!cc->Inputs().Tag("FLIP_VERTICALLY").IsEmpty()) {
|
||||
flip_vertically_ = cc->Inputs().Tag("FLIP_VERTICALLY").Get<bool>();
|
||||
}
|
||||
if (cc->Inputs().HasTag("OUTPUT_DIMENSIONS") &&
|
||||
!cc->Inputs().Tag("OUTPUT_DIMENSIONS").IsEmpty()) {
|
||||
const auto& image_size =
|
||||
cc->Inputs().Tag("OUTPUT_DIMENSIONS").Get<std::pair<int, int>>();
|
||||
output_width_ = image_size.first;
|
||||
output_height_ = image_size.second;
|
||||
if (cc->Inputs().HasTag("OUTPUT_DIMENSIONS")) {
|
||||
if (cc->Inputs().Tag("OUTPUT_DIMENSIONS").IsEmpty()) {
|
||||
return absl::OkStatus();
|
||||
} else {
|
||||
const auto& image_size =
|
||||
cc->Inputs().Tag("OUTPUT_DIMENSIONS").Get<std::pair<int, int>>();
|
||||
output_width_ = image_size.first;
|
||||
output_height_ = image_size.second;
|
||||
}
|
||||
}
|
||||
|
||||
if (use_gpu_) {
|
||||
@@ -506,6 +509,14 @@ absl::Status ImageTransformationCalculator::RenderGpu(CalculatorContext* cc) {
|
||||
ComputeOutputDimensions(input_width, input_height, &output_width,
|
||||
&output_height);
|
||||
|
||||
if (scale_mode_ == mediapipe::ScaleMode_Mode_FILL_AND_CROP) {
|
||||
const float scale =
|
||||
std::min(static_cast<float>(output_width_) / input_width,
|
||||
static_cast<float>(output_height_) / input_height);
|
||||
output_width = std::round(input_width * scale);
|
||||
output_height = std::round(input_height * scale);
|
||||
}
|
||||
|
||||
if (cc->Outputs().HasTag("LETTERBOX_PADDING")) {
|
||||
auto padding = absl::make_unique<std::array<float, 4>>();
|
||||
ComputeOutputLetterboxPadding(input_width, input_height, output_width,
|
||||
|
||||
@@ -53,7 +53,7 @@ enum { ATTRIB_VERTEX, ATTRIB_TEXTURE_POSITION, NUM_ATTRIBUTES };
|
||||
// The alpha channel can be set to a single value, or come from an image mask.
|
||||
// If the input image has an alpha channel, it will be updated.
|
||||
// If the input image doesn't have an alpha channel, one will be added.
|
||||
// Adding alpha channel to a Grayscale (single channel) input is not suported.
|
||||
// Adding alpha channel to a Grayscale (single channel) input is not supported.
|
||||
//
|
||||
// Inputs:
|
||||
// One of the following two IMAGE tags:
|
||||
|
||||
@@ -1384,6 +1384,32 @@ cc_library(
|
||||
alwayslink = 1,
|
||||
)
|
||||
|
||||
mediapipe_proto_library(
|
||||
name = "landmarks_refinement_calculator_proto",
|
||||
srcs = ["landmarks_refinement_calculator.proto"],
|
||||
deps = [
|
||||
"//mediapipe/framework:calculator_options_proto",
|
||||
"//mediapipe/framework:calculator_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "landmarks_refinement_calculator",
|
||||
srcs = ["landmarks_refinement_calculator.cc"],
|
||||
hdrs = ["landmarks_refinement_calculator.h"],
|
||||
deps = [
|
||||
":landmarks_refinement_calculator_cc_proto",
|
||||
"//mediapipe/framework:calculator_framework",
|
||||
"//mediapipe/framework/api2:node",
|
||||
"//mediapipe/framework/api2:port",
|
||||
"//mediapipe/framework/formats:landmark_cc_proto",
|
||||
"//mediapipe/framework/port:core_proto",
|
||||
"//mediapipe/framework/port:ret_check",
|
||||
"@com_google_absl//absl/memory",
|
||||
],
|
||||
alwayslink = 1,
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "refine_landmarks_from_heatmap_calculator_test",
|
||||
srcs = ["refine_landmarks_from_heatmap_calculator_test.cc"],
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
// Copyright 2021 The MediaPipe Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "mediapipe/calculators/util/landmarks_refinement_calculator.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
|
||||
#include "absl/memory/memory.h"
|
||||
#include "mediapipe/calculators/util/landmarks_refinement_calculator.pb.h"
|
||||
#include "mediapipe/framework/api2/node.h"
|
||||
#include "mediapipe/framework/api2/port.h"
|
||||
#include "mediapipe/framework/calculator_framework.h"
|
||||
#include "mediapipe/framework/port/proto_ns.h"
|
||||
#include "mediapipe/framework/port/ret_check.h"
|
||||
|
||||
namespace mediapipe {
|
||||
|
||||
namespace api2 {
|
||||
|
||||
namespace {
|
||||
|
||||
absl::StatusOr<int> GetNumberOfRefinedLandmarks(
|
||||
const proto_ns::RepeatedPtrField<
|
||||
LandmarksRefinementCalculatorOptions::Refinement>& refinements) {
|
||||
// Gather all used indexes.
|
||||
std::set<int> idxs;
|
||||
for (int i = 0; i < refinements.size(); ++i) {
|
||||
const auto& refinement = refinements.Get(i);
|
||||
for (int i = 0; i < refinement.indexes_mapping_size(); ++i) {
|
||||
idxs.insert(refinement.indexes_mapping(i));
|
||||
}
|
||||
}
|
||||
|
||||
// Check that indxes start with 0 and there is no gaps between min and max
|
||||
// indexes.
|
||||
RET_CHECK(!idxs.empty())
|
||||
<< "There should be at least one landmark in indexes mapping";
|
||||
int idxs_min = *idxs.begin();
|
||||
int idxs_max = *idxs.rbegin();
|
||||
int n_idxs = idxs.size();
|
||||
RET_CHECK_EQ(idxs_min, 0)
|
||||
<< "Indexes are expected to start with 0 instead of " << idxs_min;
|
||||
RET_CHECK_EQ(idxs_max, n_idxs - 1)
|
||||
<< "Indexes should have no gaps but " << idxs_max - n_idxs + 1
|
||||
<< " indexes are missing";
|
||||
|
||||
return n_idxs;
|
||||
}
|
||||
|
||||
void RefineXY(const proto_ns::RepeatedField<int>& indexes_mapping,
|
||||
const NormalizedLandmarkList& landmarks,
|
||||
NormalizedLandmarkList* refined_landmarks) {
|
||||
for (int i = 0; i < landmarks.landmark_size(); ++i) {
|
||||
const auto& landmark = landmarks.landmark(i);
|
||||
auto* refined_landmark =
|
||||
refined_landmarks->mutable_landmark(indexes_mapping.Get(i));
|
||||
refined_landmark->set_x(landmark.x());
|
||||
refined_landmark->set_y(landmark.y());
|
||||
}
|
||||
}
|
||||
|
||||
float GetZAverage(const NormalizedLandmarkList& landmarks,
|
||||
const proto_ns::RepeatedField<int>& indexes) {
|
||||
double z_sum = 0;
|
||||
for (int i = 0; i < indexes.size(); ++i) {
|
||||
z_sum += landmarks.landmark(indexes.Get(i)).z();
|
||||
}
|
||||
return z_sum / indexes.size();
|
||||
}
|
||||
|
||||
void RefineZ(
|
||||
const proto_ns::RepeatedField<int>& indexes_mapping,
|
||||
const LandmarksRefinementCalculatorOptions::ZRefinement& z_refinement,
|
||||
const NormalizedLandmarkList& landmarks,
|
||||
NormalizedLandmarkList* refined_landmarks) {
|
||||
if (z_refinement.has_none()) {
|
||||
// Do nothing and keep Z that is already in refined landmarks.
|
||||
} else if (z_refinement.has_copy()) {
|
||||
for (int i = 0; i < landmarks.landmark_size(); ++i) {
|
||||
refined_landmarks->mutable_landmark(indexes_mapping.Get(i))
|
||||
->set_z(landmarks.landmark(i).z());
|
||||
}
|
||||
} else if (z_refinement.has_assign_average()) {
|
||||
const float z_average =
|
||||
GetZAverage(*refined_landmarks,
|
||||
z_refinement.assign_average().indexes_for_average());
|
||||
for (int i = 0; i < indexes_mapping.size(); ++i) {
|
||||
refined_landmarks->mutable_landmark(indexes_mapping.Get(i))
|
||||
->set_z(z_average);
|
||||
}
|
||||
} else {
|
||||
CHECK(false) << "Z refinement is either not specified or not supported";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class LandmarksRefinementCalculatorImpl
|
||||
: public NodeImpl<LandmarksRefinementCalculator> {
|
||||
absl::Status Open(CalculatorContext* cc) override {
|
||||
options_ = cc->Options<LandmarksRefinementCalculatorOptions>();
|
||||
|
||||
// Validate refinements.
|
||||
for (int i = 0; i < options_.refinement_size(); ++i) {
|
||||
const auto& refinement = options_.refinement(i);
|
||||
RET_CHECK_GT(refinement.indexes_mapping_size(), 0)
|
||||
<< "Refinement " << i << " has no indexes mapping";
|
||||
RET_CHECK(refinement.has_z_refinement())
|
||||
<< "Refinement " << i << " has no Z refinement specified";
|
||||
RET_CHECK(refinement.z_refinement().has_none() ^
|
||||
refinement.z_refinement().has_copy() ^
|
||||
refinement.z_refinement().has_assign_average())
|
||||
<< "Exactly one Z refinement should be specified";
|
||||
|
||||
const auto z_refinement = refinement.z_refinement();
|
||||
if (z_refinement.has_assign_average()) {
|
||||
RET_CHECK_GT(z_refinement.assign_average().indexes_for_average_size(),
|
||||
0)
|
||||
<< "When using assign average Z refinement at least one index for "
|
||||
"averagin should be specified";
|
||||
}
|
||||
}
|
||||
|
||||
// Validate indexes mapping and get total number of refined landmarks.
|
||||
ASSIGN_OR_RETURN(n_refined_landmarks_,
|
||||
GetNumberOfRefinedLandmarks(options_.refinement()));
|
||||
|
||||
// Validate that number of refinements and landmark streams is the same.
|
||||
RET_CHECK_EQ(kLandmarks(cc).Count(), options_.refinement_size())
|
||||
<< "There are " << options_.refinement_size() << " refinements while "
|
||||
<< kLandmarks(cc).Count() << " landmark streams";
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status Process(CalculatorContext* cc) override {
|
||||
// If any of the refinement landmarks is missing - refinement won't happen.
|
||||
for (const auto& landmarks_stream : kLandmarks(cc)) {
|
||||
if (landmarks_stream.IsEmpty()) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize refined landmarks list.
|
||||
auto refined_landmarks = absl::make_unique<NormalizedLandmarkList>();
|
||||
for (int i = 0; i < n_refined_landmarks_; ++i) {
|
||||
refined_landmarks->add_landmark();
|
||||
}
|
||||
|
||||
// Apply input landmarks to outpu refined landmarks in provided order.
|
||||
for (int i = 0; i < kLandmarks(cc).Count(); ++i) {
|
||||
const auto& landmarks = kLandmarks(cc)[i].Get();
|
||||
const auto& refinement = options_.refinement(i);
|
||||
|
||||
// Check number of landmarks in mapping and stream are the same.
|
||||
RET_CHECK_EQ(landmarks.landmark_size(), refinement.indexes_mapping_size())
|
||||
<< "There are " << landmarks.landmark_size()
|
||||
<< " refinement landmarks while mapping has "
|
||||
<< refinement.indexes_mapping_size();
|
||||
|
||||
// Refine X and Y.
|
||||
RefineXY(refinement.indexes_mapping(), landmarks,
|
||||
refined_landmarks.get());
|
||||
|
||||
// Refine Z.
|
||||
RefineZ(refinement.indexes_mapping(), refinement.z_refinement(),
|
||||
landmarks, refined_landmarks.get());
|
||||
|
||||
// Visibility and presence are not currently refined and are left as `0`.
|
||||
}
|
||||
|
||||
kRefinedLandmarks(cc).Send(std::move(refined_landmarks));
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
private:
|
||||
LandmarksRefinementCalculatorOptions options_;
|
||||
int n_refined_landmarks_ = 0;
|
||||
};
|
||||
|
||||
MEDIAPIPE_NODE_IMPLEMENTATION(LandmarksRefinementCalculatorImpl);
|
||||
|
||||
} // namespace api2
|
||||
} // namespace mediapipe
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2021 The MediaPipe Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MEDIAPIPE_CALCULATORS_UTIL_LANDMARKS_REFINEMENT_CALCULATOR_H_
|
||||
#define MEDIAPIPE_CALCULATORS_UTIL_LANDMARKS_REFINEMENT_CALCULATOR_H_
|
||||
|
||||
#include "mediapipe/framework/api2/node.h"
|
||||
#include "mediapipe/framework/api2/port.h"
|
||||
#include "mediapipe/framework/formats/landmark.pb.h"
|
||||
|
||||
namespace mediapipe {
|
||||
|
||||
namespace api2 {
|
||||
|
||||
// A calculator to refine one set of landmarks with another.
|
||||
//
|
||||
// Inputs:
|
||||
// LANDMARKS: Multiple NormalizedLandmarkList to use for
|
||||
// refinement. They will be applied to the resulting REFINED_LANDMARKS in
|
||||
// the provided order. Each list should be non empty and contain the same
|
||||
// amount of landmarks as indexes in mapping. Number of lists should be the
|
||||
// same as number of refinements in options.
|
||||
//
|
||||
// Outputs:
|
||||
// REFINED_LANDMARKS: A NormalizedLandmarkList with refined landmarks. Number
|
||||
// of produced landmarks is equal to to the maximum index mapping number in
|
||||
// calculator options (calculator verifies that there are no gaps in the
|
||||
// mapping).
|
||||
//
|
||||
// Examples config:
|
||||
// node {
|
||||
// calculator: "LandmarksRefinementCalculator"
|
||||
// input_stream: "LANDMARKS:0:mesh_landmarks"
|
||||
// input_stream: "LANDMARKS:1:lips_landmarks"
|
||||
// input_stream: "LANDMARKS:2:left_eye_landmarks"
|
||||
// input_stream: "LANDMARKS:3:right_eye_landmarks"
|
||||
// output_stream: "REFINED_LANDMARKS:landmarks"
|
||||
// options: {
|
||||
// [mediapipe.LandmarksRefinementCalculatorOptions.ext] {
|
||||
// refinement: {
|
||||
// indexes_mapping: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
// z_refinement: { copy {} }
|
||||
// }
|
||||
// refinement: {
|
||||
// indexes_mapping: [0, 1, 2, 3]
|
||||
// z_refinement: { none {} }
|
||||
// }
|
||||
// refinement: {
|
||||
// indexes_mapping: [4, 5]
|
||||
// z_refinement: { none {} }
|
||||
// }
|
||||
// refinement: {
|
||||
// indexes_mapping: [6, 7]
|
||||
// z_refinement: { none {} }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
class LandmarksRefinementCalculator : public NodeIntf {
|
||||
public:
|
||||
static constexpr Input<::mediapipe::NormalizedLandmarkList>::Multiple
|
||||
kLandmarks{"LANDMARKS"};
|
||||
static constexpr Output<::mediapipe::NormalizedLandmarkList>
|
||||
kRefinedLandmarks{"REFINED_LANDMARKS"};
|
||||
|
||||
MEDIAPIPE_NODE_INTERFACE(LandmarksRefinementCalculator, kLandmarks,
|
||||
kRefinedLandmarks);
|
||||
};
|
||||
|
||||
} // namespace api2
|
||||
} // namespace mediapipe
|
||||
|
||||
#endif // MEDIAPIPE_CALCULATORS_UTIL_LANDMARKS_REFINEMENT_CALCULATOR_H_
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2021 The MediaPipe Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto2";
|
||||
|
||||
package mediapipe;
|
||||
|
||||
import "mediapipe/framework/calculator.proto";
|
||||
|
||||
message LandmarksRefinementCalculatorOptions {
|
||||
extend CalculatorOptions {
|
||||
optional LandmarksRefinementCalculatorOptions ext = 381914658;
|
||||
}
|
||||
|
||||
// Do nothing and keep those Z that are already present in the resulting set
|
||||
// of landmarks.
|
||||
message ZRefinementNone {}
|
||||
|
||||
// Simply copy Z values from the given set of landmarks to the resulting set
|
||||
// of landmarks.
|
||||
message ZRefinementCopy {}
|
||||
|
||||
// Calculate average of the specified set of landmarks in the resulting set
|
||||
// and use it as Z for all given landmarks when assigning their values to the
|
||||
// resulting set of landmarks.
|
||||
message ZRefinementAssignAverage {
|
||||
// Indexes of the resulting landmarks to use for average. Should be non
|
||||
// empty.
|
||||
repeated int32 indexes_for_average = 1;
|
||||
}
|
||||
|
||||
// Specifies the set of instructions on assigning z value from the given set
|
||||
// of landmarks to the resulting set of landmarks.
|
||||
message ZRefinement {
|
||||
// Exactly one Z refinement option should be specified.
|
||||
oneof z_refinement_options {
|
||||
ZRefinementNone none = 1;
|
||||
ZRefinementCopy copy = 2;
|
||||
ZRefinementAssignAverage assign_average = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// Specifies the set of instructions of assigning values to the resulting set
|
||||
// of landmarks.
|
||||
message Refinement {
|
||||
// Maps indexes of the given set of landmarks to indexes of the resulting
|
||||
// set of landmarks. Should be non empty and contain the same amount of
|
||||
// indexes as landmarks in the corresponding input stream.
|
||||
repeated int32 indexes_mapping = 1;
|
||||
|
||||
// Z refinement instructions.
|
||||
optional ZRefinement z_refinement = 2;
|
||||
}
|
||||
|
||||
// Refinement instructions for every landmarks input stream. Applied in the
|
||||
// same order as defined. Should be the same amount of refinements as landmark
|
||||
// input streams in the calculator. Union of index mappings should start with
|
||||
// 0 and cover a contineous range.
|
||||
repeated Refinement refinement = 1;
|
||||
}
|
||||
@@ -86,11 +86,11 @@ inline void GetMinMaxZ(const LandmarkListType& landmarks, float* z_min,
|
||||
}
|
||||
|
||||
template <class LandmarkType>
|
||||
bool IsLandmarkVisibileAndPresent(const LandmarkType& landmark,
|
||||
bool utilize_visibility,
|
||||
float visibility_threshold,
|
||||
bool utilize_presence,
|
||||
float presence_threshold) {
|
||||
bool IsLandmarkVisibleAndPresent(const LandmarkType& landmark,
|
||||
bool utilize_visibility,
|
||||
float visibility_threshold,
|
||||
bool utilize_presence,
|
||||
float presence_threshold) {
|
||||
if (utilize_visibility && landmark.has_visibility() &&
|
||||
landmark.visibility() < visibility_threshold) {
|
||||
return false;
|
||||
@@ -153,12 +153,16 @@ void AddConnectionsWithDepth(const LandmarkListType& landmarks,
|
||||
const Color& max_depth_line_color,
|
||||
RenderData* render_data) {
|
||||
for (int i = 0; i < landmark_connections.size(); i += 2) {
|
||||
if (landmark_connections[i] >= landmarks.landmark_size() ||
|
||||
landmark_connections[i + 1] >= landmarks.landmark_size()) {
|
||||
continue;
|
||||
}
|
||||
const auto& ld0 = landmarks.landmark(landmark_connections[i]);
|
||||
const auto& ld1 = landmarks.landmark(landmark_connections[i + 1]);
|
||||
if (!IsLandmarkVisibileAndPresent<LandmarkType>(
|
||||
if (!IsLandmarkVisibleAndPresent<LandmarkType>(
|
||||
ld0, utilize_visibility, visibility_threshold, utilize_presence,
|
||||
presence_threshold) ||
|
||||
!IsLandmarkVisibileAndPresent<LandmarkType>(
|
||||
!IsLandmarkVisibleAndPresent<LandmarkType>(
|
||||
ld1, utilize_visibility, visibility_threshold, utilize_presence,
|
||||
presence_threshold)) {
|
||||
continue;
|
||||
@@ -196,12 +200,16 @@ void AddConnections(const LandmarkListType& landmarks,
|
||||
const Color& connection_color, float thickness,
|
||||
bool normalized, RenderData* render_data) {
|
||||
for (int i = 0; i < landmark_connections.size(); i += 2) {
|
||||
if (landmark_connections[i] >= landmarks.landmark_size() ||
|
||||
landmark_connections[i + 1] >= landmarks.landmark_size()) {
|
||||
continue;
|
||||
}
|
||||
const auto& ld0 = landmarks.landmark(landmark_connections[i]);
|
||||
const auto& ld1 = landmarks.landmark(landmark_connections[i + 1]);
|
||||
if (!IsLandmarkVisibileAndPresent<LandmarkType>(
|
||||
if (!IsLandmarkVisibleAndPresent<LandmarkType>(
|
||||
ld0, utilize_visibility, visibility_threshold, utilize_presence,
|
||||
presence_threshold) ||
|
||||
!IsLandmarkVisibileAndPresent<LandmarkType>(
|
||||
!IsLandmarkVisibleAndPresent<LandmarkType>(
|
||||
ld1, utilize_visibility, visibility_threshold, utilize_presence,
|
||||
presence_threshold)) {
|
||||
continue;
|
||||
@@ -317,7 +325,7 @@ absl::Status LandmarksToRenderDataCalculator::Process(CalculatorContext* cc) {
|
||||
for (int i = 0; i < landmarks.landmark_size(); ++i) {
|
||||
const Landmark& landmark = landmarks.landmark(i);
|
||||
|
||||
if (!IsLandmarkVisibileAndPresent<Landmark>(
|
||||
if (!IsLandmarkVisibleAndPresent<Landmark>(
|
||||
landmark, options_.utilize_visibility(),
|
||||
options_.visibility_threshold(), options_.utilize_presence(),
|
||||
options_.presence_threshold())) {
|
||||
@@ -363,7 +371,7 @@ absl::Status LandmarksToRenderDataCalculator::Process(CalculatorContext* cc) {
|
||||
for (int i = 0; i < landmarks.landmark_size(); ++i) {
|
||||
const NormalizedLandmark& landmark = landmarks.landmark(i);
|
||||
|
||||
if (!IsLandmarkVisibileAndPresent<NormalizedLandmark>(
|
||||
if (!IsLandmarkVisibleAndPresent<NormalizedLandmark>(
|
||||
landmark, options_.utilize_visibility(),
|
||||
options_.visibility_threshold(), options_.utilize_presence(),
|
||||
options_.presence_threshold())) {
|
||||
|
||||
@@ -36,7 +36,7 @@ inline float NormalizeRadians(float angle) {
|
||||
} // namespace
|
||||
|
||||
// Performs geometric transformation to the input Rect or NormalizedRect,
|
||||
// correpsonding to input stream RECT or NORM_RECT respectively. When the input
|
||||
// corresponding to input stream RECT or NORM_RECT respectively. When the input
|
||||
// is NORM_RECT, an addition input stream IMAGE_SIZE is required, which is a
|
||||
// std::pair<int, int> representing the image width and height.
|
||||
//
|
||||
|
||||
@@ -12,5 +12,12 @@ cd /d %~dp0
|
||||
cd facemesh\src\main
|
||||
rm res
|
||||
mklink /d res ..\..\..\res
|
||||
|
||||
@rem for face detection example app.
|
||||
cd /d %~dp0
|
||||
cd facedetection\src\main
|
||||
rm res
|
||||
mklink /d res ..\..\..\res
|
||||
|
||||
dir
|
||||
pause
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.google.mediapipe.apps.facedetection"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.3'
|
||||
testImplementation 'junit:junit:4.+'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
// MediaPipe Face Detection Solution components.
|
||||
implementation 'com.google.mediapipe:solution-core:latest.release'
|
||||
implementation 'com.google.mediapipe:facedetection:latest.release'
|
||||
// MediaPipe deps
|
||||
implementation 'com.google.flogger:flogger:0.6'
|
||||
implementation 'com.google.flogger:flogger-system-backend:0.6'
|
||||
implementation 'com.google.guava:guava:27.0.1-android'
|
||||
implementation 'com.google.protobuf:protobuf-java:3.11.4'
|
||||
// CameraX core library
|
||||
def camerax_version = "1.0.0-beta10"
|
||||
implementation "androidx.camera:camera-core:$camerax_version"
|
||||
implementation "androidx.camera:camera-camera2:$camerax_version"
|
||||
implementation "androidx.camera:camera-lifecycle:$camerax_version"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.mediapipe.examples.facedetection">
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="21"
|
||||
android:targetSdkVersion="30" />
|
||||
|
||||
<!-- For loading images from gallery -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<!-- For using the camera -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="MediaPipe Face Detection"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity android:name=".MainActivity"
|
||||
android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,46 @@
|
||||
# Copyright 2021 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
android_binary(
|
||||
name = "facedetection",
|
||||
srcs = glob(["**/*.java"]),
|
||||
custom_package = "com.google.mediapipe.examples.facedetection",
|
||||
manifest = "AndroidManifest.xml",
|
||||
manifest_values = {
|
||||
"applicationId": "com.google.mediapipe.examples.facedetection",
|
||||
},
|
||||
multidex = "native",
|
||||
resource_files = ["//mediapipe/examples/android/solutions:resource_files"],
|
||||
deps = [
|
||||
"//mediapipe/framework/formats:detection_java_proto_lite",
|
||||
"//mediapipe/framework/formats:location_data_java_proto_lite",
|
||||
"//mediapipe/java/com/google/mediapipe/solutioncore:camera_input",
|
||||
"//mediapipe/java/com/google/mediapipe/solutioncore:mediapipe_jni_lib",
|
||||
"//mediapipe/java/com/google/mediapipe/solutioncore:solution_rendering",
|
||||
"//mediapipe/java/com/google/mediapipe/solutioncore:video_input",
|
||||
"//mediapipe/java/com/google/mediapipe/solutions/facedetection",
|
||||
"//third_party:androidx_appcompat",
|
||||
"//third_party:androidx_constraint_layout",
|
||||
"//third_party:opencv",
|
||||
"@maven//:androidx_activity_activity",
|
||||
"@maven//:androidx_concurrent_concurrent_futures",
|
||||
"@maven//:androidx_exifinterface_exifinterface",
|
||||
"@maven//:androidx_fragment_fragment",
|
||||
"@maven//:com_google_guava_guava",
|
||||
],
|
||||
)
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
// Copyright 2021 The MediaPipe Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.mediapipe.examples.facedetection;
|
||||
|
||||
import android.opengl.GLES20;
|
||||
import com.google.mediapipe.formats.proto.DetectionProto.Detection;
|
||||
import com.google.mediapipe.solutioncore.ResultGlRenderer;
|
||||
import com.google.mediapipe.solutions.facedetection.FaceDetectionResult;
|
||||
import com.google.mediapipe.solutions.facedetection.FaceKeypoint;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.FloatBuffer;
|
||||
|
||||
/** A custom implementation of {@link ResultGlRenderer} to render {@link FaceDetectionResult}. */
|
||||
public class FaceDetectionResultGlRenderer implements ResultGlRenderer<FaceDetectionResult> {
|
||||
private static final String TAG = "FaceDetectionResultGlRenderer";
|
||||
|
||||
private static final float[] KEYPOINT_COLOR = new float[] {1f, 0f, 0f, 1f};
|
||||
private static final float KEYPOINT_SIZE = 16f;
|
||||
private static final float[] BBOX_COLOR = new float[] {0f, 1f, 0f, 1f};
|
||||
private static final int BBOX_THICKNESS = 8;
|
||||
private static final String VERTEX_SHADER =
|
||||
"uniform mat4 uProjectionMatrix;\n"
|
||||
+ "uniform float uPointSize;\n"
|
||||
+ "attribute vec4 vPosition;\n"
|
||||
+ "void main() {\n"
|
||||
+ " gl_Position = uProjectionMatrix * vPosition;\n"
|
||||
+ " gl_PointSize = uPointSize;"
|
||||
+ "}";
|
||||
private static final String FRAGMENT_SHADER =
|
||||
"precision mediump float;\n"
|
||||
+ "uniform vec4 uColor;\n"
|
||||
+ "void main() {\n"
|
||||
+ " gl_FragColor = uColor;\n"
|
||||
+ "}";
|
||||
private int program;
|
||||
private int positionHandle;
|
||||
private int pointSizeHandle;
|
||||
private int projectionMatrixHandle;
|
||||
private int colorHandle;
|
||||
|
||||
private int loadShader(int type, String shaderCode) {
|
||||
int shader = GLES20.glCreateShader(type);
|
||||
GLES20.glShaderSource(shader, shaderCode);
|
||||
GLES20.glCompileShader(shader);
|
||||
return shader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupRendering() {
|
||||
program = GLES20.glCreateProgram();
|
||||
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER);
|
||||
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER);
|
||||
GLES20.glAttachShader(program, vertexShader);
|
||||
GLES20.glAttachShader(program, fragmentShader);
|
||||
GLES20.glLinkProgram(program);
|
||||
positionHandle = GLES20.glGetAttribLocation(program, "vPosition");
|
||||
pointSizeHandle = GLES20.glGetUniformLocation(program, "uPointSize");
|
||||
projectionMatrixHandle = GLES20.glGetUniformLocation(program, "uProjectionMatrix");
|
||||
colorHandle = GLES20.glGetUniformLocation(program, "uColor");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderResult(FaceDetectionResult result, float[] projectionMatrix) {
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
GLES20.glUseProgram(program);
|
||||
GLES20.glUniformMatrix4fv(projectionMatrixHandle, 1, false, projectionMatrix, 0);
|
||||
GLES20.glUniform1f(pointSizeHandle, KEYPOINT_SIZE);
|
||||
int numDetectedFaces = result.multiFaceDetections().size();
|
||||
for (int i = 0; i < numDetectedFaces; ++i) {
|
||||
drawDetection(result.multiFaceDetections().get(i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the shader program.
|
||||
*
|
||||
* <p>This is only necessary if one wants to release the program while keeping the context around.
|
||||
*/
|
||||
public void release() {
|
||||
GLES20.glDeleteProgram(program);
|
||||
}
|
||||
|
||||
private void drawDetection(Detection detection) {
|
||||
if (!detection.hasLocationData()) {
|
||||
return;
|
||||
}
|
||||
// Draw keypoints.
|
||||
float[] points = new float[FaceKeypoint.NUM_KEY_POINTS * 2];
|
||||
for (int i = 0; i < FaceKeypoint.NUM_KEY_POINTS; ++i) {
|
||||
points[2 * i] = detection.getLocationData().getRelativeKeypoints(i).getX();
|
||||
points[2 * i + 1] = detection.getLocationData().getRelativeKeypoints(i).getY();
|
||||
}
|
||||
GLES20.glUniform4fv(colorHandle, 1, KEYPOINT_COLOR, 0);
|
||||
FloatBuffer vertexBuffer =
|
||||
ByteBuffer.allocateDirect(points.length * 4)
|
||||
.order(ByteOrder.nativeOrder())
|
||||
.asFloatBuffer()
|
||||
.put(points);
|
||||
vertexBuffer.position(0);
|
||||
GLES20.glEnableVertexAttribArray(positionHandle);
|
||||
GLES20.glVertexAttribPointer(positionHandle, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer);
|
||||
GLES20.glDrawArrays(GLES20.GL_POINTS, 0, FaceKeypoint.NUM_KEY_POINTS);
|
||||
if (!detection.getLocationData().hasRelativeBoundingBox()) {
|
||||
return;
|
||||
}
|
||||
// Draw bounding box.
|
||||
float left = detection.getLocationData().getRelativeBoundingBox().getXmin();
|
||||
float top = detection.getLocationData().getRelativeBoundingBox().getYmin();
|
||||
float right = left + detection.getLocationData().getRelativeBoundingBox().getWidth();
|
||||
float bottom = top + detection.getLocationData().getRelativeBoundingBox().getHeight();
|
||||
drawLine(top, left, top, right);
|
||||
drawLine(bottom, left, bottom, right);
|
||||
drawLine(top, left, bottom, left);
|
||||
drawLine(top, right, bottom, right);
|
||||
}
|
||||
|
||||
private void drawLine(float y1, float x1, float y2, float x2) {
|
||||
GLES20.glUniform4fv(colorHandle, 1, BBOX_COLOR, 0);
|
||||
GLES20.glLineWidth(BBOX_THICKNESS);
|
||||
float[] vertex = {x1, y1, x2, y2};
|
||||
FloatBuffer vertexBuffer =
|
||||
ByteBuffer.allocateDirect(vertex.length * 4)
|
||||
.order(ByteOrder.nativeOrder())
|
||||
.asFloatBuffer()
|
||||
.put(vertex);
|
||||
vertexBuffer.position(0);
|
||||
GLES20.glEnableVertexAttribArray(positionHandle);
|
||||
GLES20.glVertexAttribPointer(positionHandle, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer);
|
||||
GLES20.glDrawArrays(GLES20.GL_LINES, 0, 2);
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
// Copyright 2021 The MediaPipe Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.mediapipe.examples.facedetection;
|
||||
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
import com.google.mediapipe.formats.proto.DetectionProto.Detection;
|
||||
import com.google.mediapipe.solutions.facedetection.FaceDetectionResult;
|
||||
import com.google.mediapipe.solutions.facedetection.FaceKeypoint;
|
||||
|
||||
/** An ImageView implementation for displaying {@link FaceDetectionResult}. */
|
||||
public class FaceDetectionResultImageView extends AppCompatImageView {
|
||||
private static final String TAG = "FaceDetectionResultImageView";
|
||||
|
||||
private static final int KEYPOINT_COLOR = Color.RED;
|
||||
private static final int KEYPOINT_RADIUS = 15;
|
||||
private static final int BBOX_COLOR = Color.GREEN;
|
||||
private static final int BBOX_THICKNESS = 10;
|
||||
private Bitmap latest;
|
||||
|
||||
public FaceDetectionResultImageView(Context context) {
|
||||
super(context);
|
||||
setScaleType(AppCompatImageView.ScaleType.FIT_CENTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a {@link FaceDetectionResult} to render.
|
||||
*
|
||||
* @param result a {@link FaceDetectionResult} object that contains the solution outputs and the
|
||||
* input {@link Bitmap}.
|
||||
*/
|
||||
public void setFaceDetectionResult(FaceDetectionResult result) {
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
Bitmap bmInput = result.inputBitmap();
|
||||
int width = bmInput.getWidth();
|
||||
int height = bmInput.getHeight();
|
||||
latest = Bitmap.createBitmap(width, height, bmInput.getConfig());
|
||||
Canvas canvas = new Canvas(latest);
|
||||
|
||||
canvas.drawBitmap(bmInput, new Matrix(), null);
|
||||
int numDetectedFaces = result.multiFaceDetections().size();
|
||||
for (int i = 0; i < numDetectedFaces; ++i) {
|
||||
drawDetectionOnCanvas(result.multiFaceDetections().get(i), canvas, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the image view with the latest {@link FaceDetectionResult}. */
|
||||
public void update() {
|
||||
postInvalidate();
|
||||
if (latest != null) {
|
||||
setImageBitmap(latest);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawDetectionOnCanvas(Detection detection, Canvas canvas, int width, int height) {
|
||||
if (!detection.hasLocationData()) {
|
||||
return;
|
||||
}
|
||||
// Draw keypoints.
|
||||
Paint keypointPaint = new Paint();
|
||||
keypointPaint.setColor(KEYPOINT_COLOR);
|
||||
for (int i = 0; i < FaceKeypoint.NUM_KEY_POINTS; ++i) {
|
||||
int xPixel =
|
||||
min(
|
||||
(int) (detection.getLocationData().getRelativeKeypoints(i).getX() * width),
|
||||
width - 1);
|
||||
int yPixel =
|
||||
min(
|
||||
(int) (detection.getLocationData().getRelativeKeypoints(i).getY() * height),
|
||||
height - 1);
|
||||
canvas.drawCircle(xPixel, yPixel, KEYPOINT_RADIUS, keypointPaint);
|
||||
}
|
||||
if (!detection.getLocationData().hasRelativeBoundingBox()) {
|
||||
return;
|
||||
}
|
||||
// Draw bounding box.
|
||||
Paint bboxPaint = new Paint();
|
||||
bboxPaint.setColor(BBOX_COLOR);
|
||||
bboxPaint.setStyle(Paint.Style.STROKE);
|
||||
bboxPaint.setStrokeWidth(BBOX_THICKNESS);
|
||||
float left = detection.getLocationData().getRelativeBoundingBox().getXmin() * width;
|
||||
float top = detection.getLocationData().getRelativeBoundingBox().getYmin() * height;
|
||||
float right = left + detection.getLocationData().getRelativeBoundingBox().getWidth() * width;
|
||||
float bottom = top + detection.getLocationData().getRelativeBoundingBox().getHeight() * height;
|
||||
canvas.drawRect(left, top, right, bottom, bboxPaint);
|
||||
}
|
||||
}
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
// Copyright 2021 The MediaPipe Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.mediapipe.examples.facedetection;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Matrix;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
// ContentResolver dependency
|
||||
import com.google.mediapipe.formats.proto.LocationDataProto.LocationData.RelativeKeypoint;
|
||||
import com.google.mediapipe.solutioncore.CameraInput;
|
||||
import com.google.mediapipe.solutioncore.SolutionGlSurfaceView;
|
||||
import com.google.mediapipe.solutioncore.VideoInput;
|
||||
import com.google.mediapipe.solutions.facedetection.FaceDetection;
|
||||
import com.google.mediapipe.solutions.facedetection.FaceDetectionOptions;
|
||||
import com.google.mediapipe.solutions.facedetection.FaceDetectionResult;
|
||||
import com.google.mediapipe.solutions.facedetection.FaceKeypoint;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/** Main activity of MediaPipe Face Detection app. */
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
|
||||
private FaceDetection faceDetection;
|
||||
|
||||
private enum InputSource {
|
||||
UNKNOWN,
|
||||
IMAGE,
|
||||
VIDEO,
|
||||
CAMERA,
|
||||
}
|
||||
private InputSource inputSource = InputSource.UNKNOWN;
|
||||
|
||||
// Image demo UI and image loader components.
|
||||
private ActivityResultLauncher<Intent> imageGetter;
|
||||
private FaceDetectionResultImageView imageView;
|
||||
// Video demo UI and video loader components.
|
||||
private VideoInput videoInput;
|
||||
private ActivityResultLauncher<Intent> videoGetter;
|
||||
// Live camera demo UI and camera components.
|
||||
private CameraInput cameraInput;
|
||||
|
||||
private SolutionGlSurfaceView<FaceDetectionResult> glSurfaceView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
setupStaticImageDemoUiComponents();
|
||||
setupVideoDemoUiComponents();
|
||||
setupLiveDemoUiComponents();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (inputSource == InputSource.CAMERA) {
|
||||
// Restarts the camera and the opengl surface rendering.
|
||||
cameraInput = new CameraInput(this);
|
||||
cameraInput.setNewFrameListener(textureFrame -> faceDetection.send(textureFrame));
|
||||
glSurfaceView.post(this::startCamera);
|
||||
glSurfaceView.setVisibility(View.VISIBLE);
|
||||
} else if (inputSource == InputSource.VIDEO) {
|
||||
videoInput.resume();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
if (inputSource == InputSource.CAMERA) {
|
||||
glSurfaceView.setVisibility(View.GONE);
|
||||
cameraInput.close();
|
||||
} else if (inputSource == InputSource.VIDEO) {
|
||||
videoInput.pause();
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets up the UI components for the static image demo. */
|
||||
private void setupStaticImageDemoUiComponents() {
|
||||
// The Intent to access gallery and read images as bitmap.
|
||||
imageGetter =
|
||||
registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
Intent resultIntent = result.getData();
|
||||
if (resultIntent != null) {
|
||||
if (result.getResultCode() == RESULT_OK) {
|
||||
Bitmap bitmap = null;
|
||||
try {
|
||||
bitmap =
|
||||
MediaStore.Images.Media.getBitmap(
|
||||
this.getContentResolver(), resultIntent.getData());
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Bitmap reading error:" + e);
|
||||
}
|
||||
try {
|
||||
InputStream imageData =
|
||||
this.getContentResolver().openInputStream(resultIntent.getData());
|
||||
int orientation =
|
||||
new ExifInterface(imageData)
|
||||
.getAttributeInt(
|
||||
ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||
if (orientation != ExifInterface.ORIENTATION_NORMAL) {
|
||||
Matrix matrix = new Matrix();
|
||||
switch (orientation) {
|
||||
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||
matrix.postRotate(90);
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||
matrix.postRotate(180);
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||
matrix.postRotate(270);
|
||||
break;
|
||||
default:
|
||||
matrix.postRotate(0);
|
||||
}
|
||||
bitmap =
|
||||
Bitmap.createBitmap(
|
||||
bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Bitmap rotation error:" + e);
|
||||
}
|
||||
if (bitmap != null) {
|
||||
faceDetection.send(bitmap);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Button loadImageButton = findViewById(R.id.button_load_picture);
|
||||
loadImageButton.setOnClickListener(
|
||||
v -> {
|
||||
if (inputSource != InputSource.IMAGE) {
|
||||
stopCurrentPipeline();
|
||||
setupStaticImageModePipeline();
|
||||
}
|
||||
// Reads images from gallery.
|
||||
Intent gallery =
|
||||
new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI);
|
||||
imageGetter.launch(gallery);
|
||||
});
|
||||
imageView = new FaceDetectionResultImageView(this);
|
||||
}
|
||||
|
||||
/** Sets up core workflow for static image mode. */
|
||||
private void setupStaticImageModePipeline() {
|
||||
this.inputSource = InputSource.IMAGE;
|
||||
// Initializes a new MediaPipe Face Detection solution instance in the static image mode.
|
||||
faceDetection =
|
||||
new FaceDetection(
|
||||
this,
|
||||
FaceDetectionOptions.builder()
|
||||
.setStaticImageMode(true)
|
||||
.setModelSelection(0)
|
||||
.setMinDetectionConfidence(0.5f)
|
||||
.build());
|
||||
|
||||
// Connects MediaPipe Face Detection solution to the user-defined FaceDetectionResultImageView.
|
||||
faceDetection.setResultListener(
|
||||
faceDetectionResult -> {
|
||||
logNoseTipKeypoint(faceDetectionResult, /*faceIndex=*/ 0, /*showPixelValues=*/ true);
|
||||
imageView.setFaceDetectionResult(faceDetectionResult);
|
||||
runOnUiThread(() -> imageView.update());
|
||||
});
|
||||
faceDetection.setErrorListener(
|
||||
(message, e) -> Log.e(TAG, "MediaPipe Face Detection error:" + message));
|
||||
|
||||
// Updates the preview layout.
|
||||
FrameLayout frameLayout = findViewById(R.id.preview_display_layout);
|
||||
frameLayout.removeAllViewsInLayout();
|
||||
imageView.setImageDrawable(null);
|
||||
frameLayout.addView(imageView);
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
/** Sets up the UI components for the video demo. */
|
||||
private void setupVideoDemoUiComponents() {
|
||||
// The Intent to access gallery and read a video file.
|
||||
videoGetter =
|
||||
registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
Intent resultIntent = result.getData();
|
||||
if (resultIntent != null) {
|
||||
if (result.getResultCode() == RESULT_OK) {
|
||||
glSurfaceView.post(
|
||||
() ->
|
||||
videoInput.start(
|
||||
this,
|
||||
resultIntent.getData(),
|
||||
faceDetection.getGlContext(),
|
||||
glSurfaceView.getWidth(),
|
||||
glSurfaceView.getHeight()));
|
||||
}
|
||||
}
|
||||
});
|
||||
Button loadVideoButton = findViewById(R.id.button_load_video);
|
||||
loadVideoButton.setOnClickListener(
|
||||
v -> {
|
||||
stopCurrentPipeline();
|
||||
setupStreamingModePipeline(InputSource.VIDEO);
|
||||
// Reads video from gallery.
|
||||
Intent gallery =
|
||||
new Intent(Intent.ACTION_PICK, MediaStore.Video.Media.INTERNAL_CONTENT_URI);
|
||||
videoGetter.launch(gallery);
|
||||
});
|
||||
}
|
||||
|
||||
/** Sets up the UI components for the live demo with camera input. */
|
||||
private void setupLiveDemoUiComponents() {
|
||||
Button startCameraButton = findViewById(R.id.button_start_camera);
|
||||
startCameraButton.setOnClickListener(
|
||||
v -> {
|
||||
if (inputSource == InputSource.CAMERA) {
|
||||
return;
|
||||
}
|
||||
stopCurrentPipeline();
|
||||
setupStreamingModePipeline(InputSource.CAMERA);
|
||||
});
|
||||
}
|
||||
|
||||
/** Sets up core workflow for streaming mode. */
|
||||
private void setupStreamingModePipeline(InputSource inputSource) {
|
||||
this.inputSource = inputSource;
|
||||
// Initializes a new MediaPipe Face Detection solution instance in the streaming mode.
|
||||
faceDetection =
|
||||
new FaceDetection(
|
||||
this,
|
||||
FaceDetectionOptions.builder().setStaticImageMode(false).setModelSelection(0).build());
|
||||
faceDetection.setErrorListener(
|
||||
(message, e) -> Log.e(TAG, "MediaPipe Face Detection error:" + message));
|
||||
|
||||
if (inputSource == InputSource.CAMERA) {
|
||||
cameraInput = new CameraInput(this);
|
||||
cameraInput.setNewFrameListener(textureFrame -> faceDetection.send(textureFrame));
|
||||
} else if (inputSource == InputSource.VIDEO) {
|
||||
videoInput = new VideoInput(this);
|
||||
videoInput.setNewFrameListener(textureFrame -> faceDetection.send(textureFrame));
|
||||
}
|
||||
|
||||
// Initializes a new Gl surface view with a user-defined FaceDetectionResultGlRenderer.
|
||||
glSurfaceView =
|
||||
new SolutionGlSurfaceView<>(
|
||||
this, faceDetection.getGlContext(), faceDetection.getGlMajorVersion());
|
||||
glSurfaceView.setSolutionResultRenderer(new FaceDetectionResultGlRenderer());
|
||||
glSurfaceView.setRenderInputImage(true);
|
||||
faceDetection.setResultListener(
|
||||
faceDetectionResult -> {
|
||||
logNoseTipKeypoint(faceDetectionResult, /*faceIndex=*/ 0, /*showPixelValues=*/ false);
|
||||
glSurfaceView.setRenderData(faceDetectionResult);
|
||||
glSurfaceView.requestRender();
|
||||
});
|
||||
|
||||
// The runnable to start camera after the gl surface view is attached.
|
||||
// For video input source, videoInput.start() will be called when the video uri is available.
|
||||
if (inputSource == InputSource.CAMERA) {
|
||||
glSurfaceView.post(this::startCamera);
|
||||
}
|
||||
|
||||
// Updates the preview layout.
|
||||
FrameLayout frameLayout = findViewById(R.id.preview_display_layout);
|
||||
imageView.setVisibility(View.GONE);
|
||||
frameLayout.removeAllViewsInLayout();
|
||||
frameLayout.addView(glSurfaceView);
|
||||
glSurfaceView.setVisibility(View.VISIBLE);
|
||||
frameLayout.requestLayout();
|
||||
}
|
||||
|
||||
private void startCamera() {
|
||||
cameraInput.start(
|
||||
this,
|
||||
faceDetection.getGlContext(),
|
||||
CameraInput.CameraFacing.FRONT,
|
||||
glSurfaceView.getWidth(),
|
||||
glSurfaceView.getHeight());
|
||||
}
|
||||
|
||||
private void stopCurrentPipeline() {
|
||||
if (cameraInput != null) {
|
||||
cameraInput.setNewFrameListener(null);
|
||||
cameraInput.close();
|
||||
}
|
||||
if (videoInput != null) {
|
||||
videoInput.setNewFrameListener(null);
|
||||
videoInput.close();
|
||||
}
|
||||
if (glSurfaceView != null) {
|
||||
glSurfaceView.setVisibility(View.GONE);
|
||||
}
|
||||
if (faceDetection != null) {
|
||||
faceDetection.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void logNoseTipKeypoint(
|
||||
FaceDetectionResult result, int faceIndex, boolean showPixelValues) {
|
||||
RelativeKeypoint noseTip =
|
||||
FaceDetection.getFaceKeypoint(result, faceIndex, FaceKeypoint.NOSE_TIP);
|
||||
// For Bitmaps, show the pixel values. For texture inputs, show the normalized coordinates.
|
||||
if (showPixelValues) {
|
||||
int width = result.inputBitmap().getWidth();
|
||||
int height = result.inputBitmap().getHeight();
|
||||
Log.i(
|
||||
TAG,
|
||||
String.format(
|
||||
"MediaPipe Face Detection nose tip coordinates (pixel values): x=%f, y=%f",
|
||||
noseTip.getX() * width, noseTip.getY() * height));
|
||||
} else {
|
||||
Log.i(
|
||||
TAG,
|
||||
String.format(
|
||||
"MediaPipe Face Detection nose tip normalized coordinates (value range: [0, 1]):"
|
||||
+ " x=%f, y=%f",
|
||||
noseTip.getX(), noseTip.getY()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../../res
|
||||
@@ -31,15 +31,16 @@ dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.3'
|
||||
testImplementation 'junit:junit:4.+'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
// MediaPipe hands solution API and solution-core.
|
||||
// MediaPipe Face Mesh Solution components.
|
||||
implementation 'com.google.mediapipe:solution-core:latest.release'
|
||||
implementation 'com.google.mediapipe:facemesh:latest.release'
|
||||
// MediaPipe deps
|
||||
implementation 'com.google.flogger:flogger:latest.release'
|
||||
implementation 'com.google.flogger:flogger-system-backend:latest.release'
|
||||
implementation 'com.google.flogger:flogger:0.6'
|
||||
implementation 'com.google.flogger:flogger-system-backend:0.6'
|
||||
implementation 'com.google.guava:guava:27.0.1-android'
|
||||
implementation 'com.google.protobuf:protobuf-java:3.11.4'
|
||||
// CameraX core library
|
||||
|
||||
@@ -38,6 +38,7 @@ android_binary(
|
||||
"//third_party:opencv",
|
||||
"@maven//:androidx_activity_activity",
|
||||
"@maven//:androidx_concurrent_concurrent_futures",
|
||||
"@maven//:androidx_exifinterface_exifinterface",
|
||||
"@maven//:androidx_fragment_fragment",
|
||||
"@maven//:com_google_guava_guava",
|
||||
],
|
||||
|
||||
+24
-34
@@ -15,11 +15,10 @@
|
||||
package com.google.mediapipe.examples.facemesh;
|
||||
|
||||
import android.opengl.GLES20;
|
||||
import android.opengl.Matrix;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.mediapipe.formats.proto.LandmarkProto.NormalizedLandmark;
|
||||
import com.google.mediapipe.solutioncore.ResultGlBoundary;
|
||||
import com.google.mediapipe.solutioncore.ResultGlRenderer;
|
||||
import com.google.mediapipe.solutions.facemesh.FaceMesh;
|
||||
import com.google.mediapipe.solutions.facemesh.FaceMeshConnections;
|
||||
import com.google.mediapipe.solutions.facemesh.FaceMeshResult;
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -27,7 +26,7 @@ import java.nio.ByteOrder;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.util.List;
|
||||
|
||||
/** A custom implementation of {@link ResultGlRenderer} to render MediaPope FaceMesh results. */
|
||||
/** A custom implementation of {@link ResultGlRenderer} to render {@link FaceMeshResult}. */
|
||||
public class FaceMeshResultGlRenderer implements ResultGlRenderer<FaceMeshResult> {
|
||||
private static final String TAG = "FaceMeshResultGlRenderer";
|
||||
|
||||
@@ -46,10 +45,10 @@ public class FaceMeshResultGlRenderer implements ResultGlRenderer<FaceMeshResult
|
||||
private static final float[] LIPS_COLOR = new float[] {0.9f, 0.9f, 0.9f, 1f};
|
||||
private static final int LIPS_THICKNESS = 8;
|
||||
private static final String VERTEX_SHADER =
|
||||
"uniform mat4 uTransformMatrix;\n"
|
||||
"uniform mat4 uProjectionMatrix;\n"
|
||||
+ "attribute vec4 vPosition;\n"
|
||||
+ "void main() {\n"
|
||||
+ " gl_Position = uTransformMatrix * vPosition;\n"
|
||||
+ " gl_Position = uProjectionMatrix * vPosition;\n"
|
||||
+ "}";
|
||||
private static final String FRAGMENT_SHADER =
|
||||
"precision mediump float;\n"
|
||||
@@ -59,9 +58,8 @@ public class FaceMeshResultGlRenderer implements ResultGlRenderer<FaceMeshResult
|
||||
+ "}";
|
||||
private int program;
|
||||
private int positionHandle;
|
||||
private int transformMatrixHandle;
|
||||
private int projectionMatrixHandle;
|
||||
private int colorHandle;
|
||||
private final float[] transformMatrix = new float[16];
|
||||
|
||||
private int loadShader(int type, String shaderCode) {
|
||||
int shader = GLES20.glCreateShader(type);
|
||||
@@ -79,28 +77,17 @@ public class FaceMeshResultGlRenderer implements ResultGlRenderer<FaceMeshResult
|
||||
GLES20.glAttachShader(program, fragmentShader);
|
||||
GLES20.glLinkProgram(program);
|
||||
positionHandle = GLES20.glGetAttribLocation(program, "vPosition");
|
||||
transformMatrixHandle = GLES20.glGetUniformLocation(program, "uTransformMatrix");
|
||||
projectionMatrixHandle = GLES20.glGetUniformLocation(program, "uProjectionMatrix");
|
||||
colorHandle = GLES20.glGetUniformLocation(program, "uColor");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderResult(FaceMeshResult result, ResultGlBoundary boundary) {
|
||||
public void renderResult(FaceMeshResult result, float[] projectionMatrix) {
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
GLES20.glUseProgram(program);
|
||||
// Sets the transform matrix to align the result rendering with the scaled output texture.
|
||||
// Also flips the rendering vertically since OpenGL assumes the coordinate origin is at the
|
||||
// bottom-left corner, whereas MediaPipe landmark data assumes the coordinate origin is at the
|
||||
// top-left corner.
|
||||
Matrix.setIdentityM(transformMatrix, 0);
|
||||
Matrix.scaleM(
|
||||
transformMatrix,
|
||||
0,
|
||||
2 / (boundary.right() - boundary.left()),
|
||||
-2 / (boundary.top() - boundary.bottom()),
|
||||
1.0f);
|
||||
GLES20.glUniformMatrix4fv(transformMatrixHandle, 1, false, transformMatrix, 0);
|
||||
GLES20.glUniformMatrix4fv(projectionMatrixHandle, 1, false, projectionMatrix, 0);
|
||||
|
||||
int numFaces = result.multiFaceLandmarks().size();
|
||||
for (int i = 0; i < numFaces; ++i) {
|
||||
@@ -126,7 +113,7 @@ public class FaceMeshResultGlRenderer implements ResultGlRenderer<FaceMeshResult
|
||||
LEFT_EYE_THICKNESS);
|
||||
drawLandmarks(
|
||||
result.multiFaceLandmarks().get(i).getLandmarkList(),
|
||||
FaceMeshConnections.FACEMESH_LEFT_EYEBR0W,
|
||||
FaceMeshConnections.FACEMESH_LEFT_EYEBROW,
|
||||
LEFT_EYEBROW_COLOR,
|
||||
LEFT_EYEBROW_THICKNESS);
|
||||
drawLandmarks(
|
||||
@@ -139,11 +126,24 @@ public class FaceMeshResultGlRenderer implements ResultGlRenderer<FaceMeshResult
|
||||
FaceMeshConnections.FACEMESH_LIPS,
|
||||
LIPS_COLOR,
|
||||
LIPS_THICKNESS);
|
||||
if (result.multiFaceLandmarks().get(i).getLandmarkCount()
|
||||
== FaceMesh.FACEMESH_NUM_LANDMARKS_WITH_IRISES) {
|
||||
drawLandmarks(
|
||||
result.multiFaceLandmarks().get(i).getLandmarkList(),
|
||||
FaceMeshConnections.FACEMESH_RIGHT_IRIS,
|
||||
RIGHT_EYE_COLOR,
|
||||
RIGHT_EYE_THICKNESS);
|
||||
drawLandmarks(
|
||||
result.multiFaceLandmarks().get(i).getLandmarkList(),
|
||||
FaceMeshConnections.FACEMESH_LEFT_IRIS,
|
||||
LEFT_EYE_COLOR,
|
||||
LEFT_EYE_THICKNESS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls this to delete the shader program.
|
||||
* Deletes the shader program.
|
||||
*
|
||||
* <p>This is only necessary if one wants to release the program while keeping the context around.
|
||||
*/
|
||||
@@ -159,13 +159,9 @@ public class FaceMeshResultGlRenderer implements ResultGlRenderer<FaceMeshResult
|
||||
GLES20.glUniform4fv(colorHandle, 1, colorArray, 0);
|
||||
GLES20.glLineWidth(thickness);
|
||||
for (FaceMeshConnections.Connection c : connections) {
|
||||
float[] vertex = new float[4];
|
||||
NormalizedLandmark start = faceLandmarkList.get(c.start());
|
||||
vertex[0] = normalizedLandmarkValue(start.getX());
|
||||
vertex[1] = normalizedLandmarkValue(start.getY());
|
||||
NormalizedLandmark end = faceLandmarkList.get(c.end());
|
||||
vertex[2] = normalizedLandmarkValue(end.getX());
|
||||
vertex[3] = normalizedLandmarkValue(end.getY());
|
||||
float[] vertex = {start.getX(), start.getY(), end.getX(), end.getY()};
|
||||
FloatBuffer vertexBuffer =
|
||||
ByteBuffer.allocateDirect(vertex.length * 4)
|
||||
.order(ByteOrder.nativeOrder())
|
||||
@@ -177,10 +173,4 @@ public class FaceMeshResultGlRenderer implements ResultGlRenderer<FaceMeshResult
|
||||
GLES20.glDrawArrays(GLES20.GL_LINES, 0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalizes the value from the landmark value range:[0, 1] to the standard OpenGL coordinate
|
||||
// value range: [-1, 1].
|
||||
private float normalizedLandmarkValue(float value) {
|
||||
return value * 2 - 1;
|
||||
}
|
||||
}
|
||||
|
||||
+21
-4
@@ -24,11 +24,12 @@ import androidx.appcompat.widget.AppCompatImageView;
|
||||
import android.util.Size;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.mediapipe.formats.proto.LandmarkProto.NormalizedLandmark;
|
||||
import com.google.mediapipe.solutions.facemesh.FaceMesh;
|
||||
import com.google.mediapipe.solutions.facemesh.FaceMeshConnections;
|
||||
import com.google.mediapipe.solutions.facemesh.FaceMeshResult;
|
||||
import java.util.List;
|
||||
|
||||
/** An ImageView implementation for displaying MediaPipe FaceMesh results. */
|
||||
/** An ImageView implementation for displaying {@link FaceMeshResult}. */
|
||||
public class FaceMeshResultImageView extends AppCompatImageView {
|
||||
private static final String TAG = "FaceMeshResultImageView";
|
||||
|
||||
@@ -103,7 +104,7 @@ public class FaceMeshResultImageView extends AppCompatImageView {
|
||||
drawLandmarksOnCanvas(
|
||||
canvas,
|
||||
result.multiFaceLandmarks().get(i).getLandmarkList(),
|
||||
FaceMeshConnections.FACEMESH_LEFT_EYEBR0W,
|
||||
FaceMeshConnections.FACEMESH_LEFT_EYEBROW,
|
||||
imageSize,
|
||||
LEFT_EYEBROW_COLOR,
|
||||
LEFT_EYEBROW_THICKNESS);
|
||||
@@ -121,10 +122,27 @@ public class FaceMeshResultImageView extends AppCompatImageView {
|
||||
imageSize,
|
||||
LIPS_COLOR,
|
||||
LIPS_THICKNESS);
|
||||
if (result.multiFaceLandmarks().get(i).getLandmarkCount()
|
||||
== FaceMesh.FACEMESH_NUM_LANDMARKS_WITH_IRISES) {
|
||||
drawLandmarksOnCanvas(
|
||||
canvas,
|
||||
result.multiFaceLandmarks().get(i).getLandmarkList(),
|
||||
FaceMeshConnections.FACEMESH_RIGHT_IRIS,
|
||||
imageSize,
|
||||
RIGHT_EYE_COLOR,
|
||||
RIGHT_EYE_THICKNESS);
|
||||
drawLandmarksOnCanvas(
|
||||
canvas,
|
||||
result.multiFaceLandmarks().get(i).getLandmarkList(),
|
||||
FaceMeshConnections.FACEMESH_LEFT_IRIS,
|
||||
imageSize,
|
||||
LEFT_EYE_COLOR,
|
||||
LEFT_EYE_THICKNESS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the image view with the latest facemesh result. */
|
||||
/** Updates the image view with the latest {@link FaceMeshResult}. */
|
||||
public void update() {
|
||||
postInvalidate();
|
||||
if (latest != null) {
|
||||
@@ -132,7 +150,6 @@ public class FaceMeshResultImageView extends AppCompatImageView {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Better hand landmark and hand connection drawing.
|
||||
private void drawLandmarksOnCanvas(
|
||||
Canvas canvas,
|
||||
List<NormalizedLandmark> faceLandmarkList,
|
||||
|
||||
+49
-14
@@ -16,6 +16,7 @@ package com.google.mediapipe.examples.facemesh;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Matrix;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -25,6 +26,8 @@ import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
// ContentResolver dependency
|
||||
import com.google.mediapipe.formats.proto.LandmarkProto.NormalizedLandmark;
|
||||
import com.google.mediapipe.solutioncore.CameraInput;
|
||||
import com.google.mediapipe.solutioncore.SolutionGlSurfaceView;
|
||||
@@ -33,8 +36,9 @@ import com.google.mediapipe.solutions.facemesh.FaceMesh;
|
||||
import com.google.mediapipe.solutions.facemesh.FaceMeshOptions;
|
||||
import com.google.mediapipe.solutions.facemesh.FaceMeshResult;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/** Main activity of MediaPipe FaceMesh app. */
|
||||
/** Main activity of MediaPipe Face Mesh app. */
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
|
||||
@@ -57,12 +61,14 @@ public class MainActivity extends AppCompatActivity {
|
||||
private ActivityResultLauncher<Intent> videoGetter;
|
||||
// Live camera demo UI and camera components.
|
||||
private CameraInput cameraInput;
|
||||
|
||||
private SolutionGlSurfaceView<FaceMeshResult> glSurfaceView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
// TODO: Add a toggle to switch between the original face mesh and attention mesh.
|
||||
setupStaticImageDemoUiComponents();
|
||||
setupVideoDemoUiComponents();
|
||||
setupLiveDemoUiComponents();
|
||||
@@ -111,6 +117,35 @@ public class MainActivity extends AppCompatActivity {
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Bitmap reading error:" + e);
|
||||
}
|
||||
try {
|
||||
InputStream imageData =
|
||||
this.getContentResolver().openInputStream(resultIntent.getData());
|
||||
int orientation =
|
||||
new ExifInterface(imageData)
|
||||
.getAttributeInt(
|
||||
ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||
if (orientation != ExifInterface.ORIENTATION_NORMAL) {
|
||||
Matrix matrix = new Matrix();
|
||||
switch (orientation) {
|
||||
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||
matrix.postRotate(90);
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||
matrix.postRotate(180);
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||
matrix.postRotate(270);
|
||||
break;
|
||||
default:
|
||||
matrix.postRotate(0);
|
||||
}
|
||||
bitmap =
|
||||
Bitmap.createBitmap(
|
||||
bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Bitmap rotation error:" + e);
|
||||
}
|
||||
if (bitmap != null) {
|
||||
facemesh.send(bitmap);
|
||||
}
|
||||
@@ -132,26 +167,27 @@ public class MainActivity extends AppCompatActivity {
|
||||
imageView = new FaceMeshResultImageView(this);
|
||||
}
|
||||
|
||||
/** The core MediaPipe FaceMesh setup workflow for its static image mode. */
|
||||
/** Sets up core workflow for static image mode. */
|
||||
private void setupStaticImageModePipeline() {
|
||||
this.inputSource = InputSource.IMAGE;
|
||||
// Initializes a new MediaPipe FaceMesh instance in the static image mode.
|
||||
// Initializes a new MediaPipe Face Mesh solution instance in the static image mode.
|
||||
facemesh =
|
||||
new FaceMesh(
|
||||
this,
|
||||
FaceMeshOptions.builder()
|
||||
.setMode(FaceMeshOptions.STATIC_IMAGE_MODE)
|
||||
.setStaticImageMode(true)
|
||||
.setRefineLandmarks(true)
|
||||
.setRunOnGpu(RUN_ON_GPU)
|
||||
.build());
|
||||
|
||||
// Connects MediaPipe FaceMesh to the user-defined FaceMeshResultImageView.
|
||||
// Connects MediaPipe Face Mesh solution to the user-defined FaceMeshResultImageView.
|
||||
facemesh.setResultListener(
|
||||
faceMeshResult -> {
|
||||
logNoseLandmark(faceMeshResult, /*showPixelValues=*/ true);
|
||||
imageView.setFaceMeshResult(faceMeshResult);
|
||||
runOnUiThread(() -> imageView.update());
|
||||
});
|
||||
facemesh.setErrorListener((message, e) -> Log.e(TAG, "MediaPipe FaceMesh error:" + message));
|
||||
facemesh.setErrorListener((message, e) -> Log.e(TAG, "MediaPipe Face Mesh error:" + message));
|
||||
|
||||
// Updates the preview layout.
|
||||
FrameLayout frameLayout = findViewById(R.id.preview_display_layout);
|
||||
@@ -207,25 +243,24 @@ public class MainActivity extends AppCompatActivity {
|
||||
});
|
||||
}
|
||||
|
||||
/** The core MediaPipe FaceMesh setup workflow for its streaming mode. */
|
||||
/** Sets up core workflow for streaming mode. */
|
||||
private void setupStreamingModePipeline(InputSource inputSource) {
|
||||
this.inputSource = inputSource;
|
||||
// Initializes a new MediaPipe FaceMesh instance in the streaming mode.
|
||||
// Initializes a new MediaPipe Face Mesh solution instance in the streaming mode.
|
||||
facemesh =
|
||||
new FaceMesh(
|
||||
this,
|
||||
FaceMeshOptions.builder()
|
||||
.setMode(FaceMeshOptions.STREAMING_MODE)
|
||||
.setStaticImageMode(false)
|
||||
.setRefineLandmarks(true)
|
||||
.setRunOnGpu(RUN_ON_GPU)
|
||||
.build());
|
||||
facemesh.setErrorListener((message, e) -> Log.e(TAG, "MediaPipe FaceMesh error:" + message));
|
||||
facemesh.setErrorListener((message, e) -> Log.e(TAG, "MediaPipe Face Mesh error:" + message));
|
||||
|
||||
if (inputSource == InputSource.CAMERA) {
|
||||
// Initializes a new CameraInput instance and connects it to MediaPipe FaceMesh.
|
||||
cameraInput = new CameraInput(this);
|
||||
cameraInput.setNewFrameListener(textureFrame -> facemesh.send(textureFrame));
|
||||
} else if (inputSource == InputSource.VIDEO) {
|
||||
// Initializes a new VideoInput instance and connects it to MediaPipe FaceMesh.
|
||||
videoInput = new VideoInput(this);
|
||||
videoInput.setNewFrameListener(textureFrame -> facemesh.send(textureFrame));
|
||||
}
|
||||
@@ -295,13 +330,13 @@ public class MainActivity extends AppCompatActivity {
|
||||
Log.i(
|
||||
TAG,
|
||||
String.format(
|
||||
"MediaPipe FaceMesh nose coordinates (pixel values): x=%f, y=%f",
|
||||
"MediaPipe Face Mesh nose coordinates (pixel values): x=%f, y=%f",
|
||||
noseLandmark.getX() * width, noseLandmark.getY() * height));
|
||||
} else {
|
||||
Log.i(
|
||||
TAG,
|
||||
String.format(
|
||||
"MediaPipe FaceMesh nose normalized coordinates (value range: [0, 1]): x=%f, y=%f",
|
||||
"MediaPipe Face Mesh nose normalized coordinates (value range: [0, 1]): x=%f, y=%f",
|
||||
noseLandmark.getX(), noseLandmark.getY()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,15 +31,16 @@ dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.3'
|
||||
testImplementation 'junit:junit:4.+'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
// MediaPipe hands solution API and solution-core.
|
||||
// MediaPipe Hands Solution components.
|
||||
implementation 'com.google.mediapipe:solution-core:latest.release'
|
||||
implementation 'com.google.mediapipe:hands:latest.release'
|
||||
// MediaPipe deps
|
||||
implementation 'com.google.flogger:flogger:latest.release'
|
||||
implementation 'com.google.flogger:flogger-system-backend:latest.release'
|
||||
implementation 'com.google.flogger:flogger:0.6'
|
||||
implementation 'com.google.flogger:flogger-system-backend:0.6'
|
||||
implementation 'com.google.guava:guava:27.0.1-android'
|
||||
implementation 'com.google.protobuf:protobuf-java:3.11.4'
|
||||
// CameraX core library
|
||||
|
||||
@@ -38,6 +38,7 @@ android_binary(
|
||||
"//third_party:opencv",
|
||||
"@maven//:androidx_activity_activity",
|
||||
"@maven//:androidx_concurrent_concurrent_futures",
|
||||
"@maven//:androidx_exifinterface_exifinterface",
|
||||
"@maven//:androidx_fragment_fragment",
|
||||
"@maven//:com_google_guava_guava",
|
||||
],
|
||||
|
||||
+9
-34
@@ -15,9 +15,7 @@
|
||||
package com.google.mediapipe.examples.hands;
|
||||
|
||||
import android.opengl.GLES20;
|
||||
import android.opengl.Matrix;
|
||||
import com.google.mediapipe.formats.proto.LandmarkProto.NormalizedLandmark;
|
||||
import com.google.mediapipe.solutioncore.ResultGlBoundary;
|
||||
import com.google.mediapipe.solutioncore.ResultGlRenderer;
|
||||
import com.google.mediapipe.solutions.hands.Hands;
|
||||
import com.google.mediapipe.solutions.hands.HandsResult;
|
||||
@@ -26,16 +24,16 @@ import java.nio.ByteOrder;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.util.List;
|
||||
|
||||
/** A custom implementation of {@link ResultGlRenderer} to render MediaPope Hands results. */
|
||||
/** A custom implementation of {@link ResultGlRenderer} to render {@link HandsResult}. */
|
||||
public class HandsResultGlRenderer implements ResultGlRenderer<HandsResult> {
|
||||
private static final String TAG = "HandsResultGlRenderer";
|
||||
|
||||
private static final float CONNECTION_THICKNESS = 20.0f;
|
||||
private static final String VERTEX_SHADER =
|
||||
"uniform mat4 uTransformMatrix;\n"
|
||||
"uniform mat4 uProjectionMatrix;\n"
|
||||
+ "attribute vec4 vPosition;\n"
|
||||
+ "void main() {\n"
|
||||
+ " gl_Position = uTransformMatrix * vPosition;\n"
|
||||
+ " gl_Position = uProjectionMatrix * vPosition;\n"
|
||||
+ "}";
|
||||
private static final String FRAGMENT_SHADER =
|
||||
"precision mediump float;\n"
|
||||
@@ -44,8 +42,7 @@ public class HandsResultGlRenderer implements ResultGlRenderer<HandsResult> {
|
||||
+ "}";
|
||||
private int program;
|
||||
private int positionHandle;
|
||||
private int transformMatrixHandle;
|
||||
private final float[] transformMatrix = new float[16];
|
||||
private int projectionMatrixHandle;
|
||||
|
||||
private int loadShader(int type, String shaderCode) {
|
||||
int shader = GLES20.glCreateShader(type);
|
||||
@@ -63,27 +60,16 @@ public class HandsResultGlRenderer implements ResultGlRenderer<HandsResult> {
|
||||
GLES20.glAttachShader(program, fragmentShader);
|
||||
GLES20.glLinkProgram(program);
|
||||
positionHandle = GLES20.glGetAttribLocation(program, "vPosition");
|
||||
transformMatrixHandle = GLES20.glGetUniformLocation(program, "uTransformMatrix");
|
||||
projectionMatrixHandle = GLES20.glGetUniformLocation(program, "uProjectionMatrix");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderResult(HandsResult result, ResultGlBoundary boundary) {
|
||||
public void renderResult(HandsResult result, float[] projectionMatrix) {
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
GLES20.glUseProgram(program);
|
||||
// Sets the transform matrix to align the result rendering with the scaled output texture.
|
||||
// Also flips the rendering vertically since OpenGL assumes the coordinate origin is at the
|
||||
// bottom-left corner, whereas MediaPipe landmark data assumes the coordinate origin is at the
|
||||
// top-left corner.
|
||||
Matrix.setIdentityM(transformMatrix, 0);
|
||||
Matrix.scaleM(
|
||||
transformMatrix,
|
||||
0,
|
||||
2 / (boundary.right() - boundary.left()),
|
||||
-2 / (boundary.top() - boundary.bottom()),
|
||||
1.0f);
|
||||
GLES20.glUniformMatrix4fv(transformMatrixHandle, 1, false, transformMatrix, 0);
|
||||
GLES20.glUniformMatrix4fv(projectionMatrixHandle, 1, false, projectionMatrix, 0);
|
||||
GLES20.glLineWidth(CONNECTION_THICKNESS);
|
||||
|
||||
int numHands = result.multiHandLandmarks().size();
|
||||
@@ -93,7 +79,7 @@ public class HandsResultGlRenderer implements ResultGlRenderer<HandsResult> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls this to delete the shader program.
|
||||
* Deletes the shader program.
|
||||
*
|
||||
* <p>This is only necessary if one wants to release the program while keeping the context around.
|
||||
*/
|
||||
@@ -101,16 +87,11 @@ public class HandsResultGlRenderer implements ResultGlRenderer<HandsResult> {
|
||||
GLES20.glDeleteProgram(program);
|
||||
}
|
||||
|
||||
// TODO: Better hand landmark and hand connection drawing.
|
||||
private void drawLandmarks(List<NormalizedLandmark> handLandmarkList) {
|
||||
for (Hands.Connection c : Hands.HAND_CONNECTIONS) {
|
||||
float[] vertex = new float[4];
|
||||
NormalizedLandmark start = handLandmarkList.get(c.start());
|
||||
vertex[0] = normalizedLandmarkValue(start.getX());
|
||||
vertex[1] = normalizedLandmarkValue(start.getY());
|
||||
NormalizedLandmark end = handLandmarkList.get(c.end());
|
||||
vertex[2] = normalizedLandmarkValue(end.getX());
|
||||
vertex[3] = normalizedLandmarkValue(end.getY());
|
||||
float[] vertex = {start.getX(), start.getY(), end.getX(), end.getY()};
|
||||
FloatBuffer vertexBuffer =
|
||||
ByteBuffer.allocateDirect(vertex.length * 4)
|
||||
.order(ByteOrder.nativeOrder())
|
||||
@@ -122,10 +103,4 @@ public class HandsResultGlRenderer implements ResultGlRenderer<HandsResult> {
|
||||
GLES20.glDrawArrays(GLES20.GL_LINES, 0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalizes the value from the landmark value range:[0, 1] to the standard OpenGL coordinate
|
||||
// value range: [-1, 1].
|
||||
private float normalizedLandmarkValue(float value) {
|
||||
return value * 2 - 1;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -27,7 +27,7 @@ import com.google.mediapipe.solutions.hands.Hands;
|
||||
import com.google.mediapipe.solutions.hands.HandsResult;
|
||||
import java.util.List;
|
||||
|
||||
/** An ImageView implementation for displaying MediaPipe Hands results. */
|
||||
/** An ImageView implementation for displaying {@link HandsResult}. */
|
||||
public class HandsResultImageView extends AppCompatImageView {
|
||||
private static final String TAG = "HandsResultImageView";
|
||||
|
||||
@@ -66,7 +66,7 @@ public class HandsResultImageView extends AppCompatImageView {
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the image view with the latest hands result. */
|
||||
/** Updates the image view with the latest {@link HandsResult}. */
|
||||
public void update() {
|
||||
postInvalidate();
|
||||
if (latest != null) {
|
||||
@@ -74,7 +74,6 @@ public class HandsResultImageView extends AppCompatImageView {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Better hand landmark and hand connection drawing.
|
||||
private void drawLandmarksOnCanvas(
|
||||
List<NormalizedLandmark> handLandmarkList, Canvas canvas, int width, int height) {
|
||||
// Draw connections.
|
||||
|
||||
+41
-9
@@ -16,6 +16,7 @@ package com.google.mediapipe.examples.hands;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Matrix;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -25,6 +26,8 @@ import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
// ContentResolver dependency
|
||||
import com.google.mediapipe.formats.proto.LandmarkProto.NormalizedLandmark;
|
||||
import com.google.mediapipe.solutioncore.CameraInput;
|
||||
import com.google.mediapipe.solutioncore.SolutionGlSurfaceView;
|
||||
@@ -34,6 +37,7 @@ import com.google.mediapipe.solutions.hands.Hands;
|
||||
import com.google.mediapipe.solutions.hands.HandsOptions;
|
||||
import com.google.mediapipe.solutions.hands.HandsResult;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/** Main activity of MediaPipe Hands app. */
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
@@ -59,6 +63,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
private ActivityResultLauncher<Intent> videoGetter;
|
||||
// Live camera demo UI and camera components.
|
||||
private CameraInput cameraInput;
|
||||
|
||||
private SolutionGlSurfaceView<HandsResult> glSurfaceView;
|
||||
|
||||
@Override
|
||||
@@ -113,6 +118,35 @@ public class MainActivity extends AppCompatActivity {
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Bitmap reading error:" + e);
|
||||
}
|
||||
try {
|
||||
InputStream imageData =
|
||||
this.getContentResolver().openInputStream(resultIntent.getData());
|
||||
int orientation =
|
||||
new ExifInterface(imageData)
|
||||
.getAttributeInt(
|
||||
ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||
if (orientation != ExifInterface.ORIENTATION_NORMAL) {
|
||||
Matrix matrix = new Matrix();
|
||||
switch (orientation) {
|
||||
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||
matrix.postRotate(90);
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||
matrix.postRotate(180);
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||
matrix.postRotate(270);
|
||||
break;
|
||||
default:
|
||||
matrix.postRotate(0);
|
||||
}
|
||||
bitmap =
|
||||
Bitmap.createBitmap(
|
||||
bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Bitmap rotation error:" + e);
|
||||
}
|
||||
if (bitmap != null) {
|
||||
hands.send(bitmap);
|
||||
}
|
||||
@@ -134,20 +168,20 @@ public class MainActivity extends AppCompatActivity {
|
||||
imageView = new HandsResultImageView(this);
|
||||
}
|
||||
|
||||
/** The core MediaPipe Hands setup workflow for its static image mode. */
|
||||
/** Sets up core workflow for static image mode. */
|
||||
private void setupStaticImageModePipeline() {
|
||||
this.inputSource = InputSource.IMAGE;
|
||||
// Initializes a new MediaPipe Hands instance in the static image mode.
|
||||
// Initializes a new MediaPipe Hands solution instance in the static image mode.
|
||||
hands =
|
||||
new Hands(
|
||||
this,
|
||||
HandsOptions.builder()
|
||||
.setMode(HandsOptions.STATIC_IMAGE_MODE)
|
||||
.setStaticImageMode(true)
|
||||
.setMaxNumHands(1)
|
||||
.setRunOnGpu(RUN_ON_GPU)
|
||||
.build());
|
||||
|
||||
// Connects MediaPipe Hands to the user-defined HandsResultImageView.
|
||||
// Connects MediaPipe Hands solution to the user-defined HandsResultImageView.
|
||||
hands.setResultListener(
|
||||
handsResult -> {
|
||||
logWristLandmark(handsResult, /*showPixelValues=*/ true);
|
||||
@@ -210,26 +244,24 @@ public class MainActivity extends AppCompatActivity {
|
||||
});
|
||||
}
|
||||
|
||||
/** The core MediaPipe Hands setup workflow for its streaming mode. */
|
||||
/** Sets up core workflow for streaming mode. */
|
||||
private void setupStreamingModePipeline(InputSource inputSource) {
|
||||
this.inputSource = inputSource;
|
||||
// Initializes a new MediaPipe Hands instance in the streaming mode.
|
||||
// Initializes a new MediaPipe Hands solution instance in the streaming mode.
|
||||
hands =
|
||||
new Hands(
|
||||
this,
|
||||
HandsOptions.builder()
|
||||
.setMode(HandsOptions.STREAMING_MODE)
|
||||
.setStaticImageMode(false)
|
||||
.setMaxNumHands(1)
|
||||
.setRunOnGpu(RUN_ON_GPU)
|
||||
.build());
|
||||
hands.setErrorListener((message, e) -> Log.e(TAG, "MediaPipe Hands error:" + message));
|
||||
|
||||
if (inputSource == InputSource.CAMERA) {
|
||||
// Initializes a new CameraInput instance and connects it to MediaPipe Hands.
|
||||
cameraInput = new CameraInput(this);
|
||||
cameraInput.setNewFrameListener(textureFrame -> hands.send(textureFrame));
|
||||
} else if (inputSource == InputSource.VIDEO) {
|
||||
// Initializes a new VideoInput instance and connects it to MediaPipe Hands.
|
||||
videoInput = new VideoInput(this);
|
||||
videoInput.setNewFrameListener(textureFrame -> hands.send(textureFrame));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
rootProject.name = "mediapipe-solutions-examples"
|
||||
include ':hands'
|
||||
include ':facedetection'
|
||||
include ':facemesh'
|
||||
include ':hands'
|
||||
|
||||
@@ -37,7 +37,7 @@ android_binary(
|
||||
srcs = glob(["*.java"]),
|
||||
assets = [
|
||||
"//mediapipe/graphs/face_mesh:face_mesh_mobile_gpu.binarypb",
|
||||
"//mediapipe/modules/face_landmark:face_landmark.tflite",
|
||||
"//mediapipe/modules/face_landmark:face_landmark_with_attention.tflite",
|
||||
"//mediapipe/modules/face_detection:face_detection_short_range.tflite",
|
||||
],
|
||||
assets_dir = "",
|
||||
|
||||
@@ -63,7 +63,7 @@ objc_library(
|
||||
data = [
|
||||
"//mediapipe/graphs/face_mesh:face_mesh_mobile_gpu.binarypb",
|
||||
"//mediapipe/modules/face_detection:face_detection_short_range.tflite",
|
||||
"//mediapipe/modules/face_landmark:face_landmark.tflite",
|
||||
"//mediapipe/modules/face_landmark:face_landmark_with_attention.tflite",
|
||||
],
|
||||
deps = [
|
||||
"//mediapipe/examples/ios/common:CommonMediaPipeAppLibrary",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#if !MEDIAPIPE_DISABLE_GPU
|
||||
#ifdef __APPLE__
|
||||
#include "mediapipe/objc/CFHolder.h"
|
||||
#include "mediapipe/objc/util.h"
|
||||
#endif // __APPLE__
|
||||
#endif // !MEDIAPIPE_DISABLE_GPU
|
||||
|
||||
|
||||
@@ -89,6 +89,18 @@ cc_library(
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "commandlineflags",
|
||||
hdrs = [
|
||||
"commandlineflags.h",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//third_party:glog",
|
||||
"@com_google_absl//absl/flags:flag",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "core_proto",
|
||||
hdrs = [
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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_PORT_COMMANDLINEFLAGS_H_
|
||||
#define MEDIAPIPE_PORT_COMMANDLINEFLAGS_H_
|
||||
|
||||
#include "gflags/gflags.h"
|
||||
namespace absl {
|
||||
template <typename T>
|
||||
T GetFlag(const T& f) {
|
||||
return f;
|
||||
}
|
||||
template <typename T, typename U>
|
||||
void SetFlag(T* f, const U& u) {
|
||||
*f = u;
|
||||
}
|
||||
} // namespace absl
|
||||
|
||||
#endif // MEDIAPIPE_PORT_COMMANDLINEFLAGS_H_
|
||||
@@ -202,6 +202,7 @@ cc_library(
|
||||
"//mediapipe/framework:packet_type",
|
||||
"//mediapipe/framework/port:advanced_proto",
|
||||
"//mediapipe/framework/port:any_proto",
|
||||
"//mediapipe/framework/port:ret_check",
|
||||
"//mediapipe/framework/port:status",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_absl//absl/strings",
|
||||
@@ -291,7 +292,9 @@ mediapipe_cc_test(
|
||||
data = [":node_chain_subgraph.proto"],
|
||||
requires_full_emulation = False,
|
||||
deps = [
|
||||
":options_field_util",
|
||||
":options_registry",
|
||||
":options_syntax_util",
|
||||
":options_util",
|
||||
"//mediapipe/calculators/core:flow_limiter_calculator",
|
||||
"//mediapipe/calculators/core:flow_limiter_calculator_cc_proto",
|
||||
@@ -305,8 +308,8 @@ mediapipe_cc_test(
|
||||
"//mediapipe/framework/port:status",
|
||||
"//mediapipe/framework/testdata:night_light_calculator_options_lib",
|
||||
"//mediapipe/framework/tool:node_chain_subgraph_options_lib",
|
||||
"//mediapipe/framework/tool:options_syntax_util",
|
||||
"//mediapipe/util:header_util",
|
||||
"@com_google_absl//absl/strings",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "mediapipe/framework/packet.h"
|
||||
#include "mediapipe/framework/packet_type.h"
|
||||
#include "mediapipe/framework/port/advanced_proto_inc.h"
|
||||
#include "mediapipe/framework/port/any_proto.h"
|
||||
#include "mediapipe/framework/port/canonical_errors.h"
|
||||
#include "mediapipe/framework/port/ret_check.h"
|
||||
#include "mediapipe/framework/port/status.h"
|
||||
#include "mediapipe/framework/tool/name_util.h"
|
||||
#include "mediapipe/framework/tool/proto_util_lite.h"
|
||||
@@ -31,6 +33,9 @@ using ::mediapipe::proto_ns::io::StringOutputStream;
|
||||
// Utility functions for OptionsFieldUtil.
|
||||
namespace {
|
||||
|
||||
// The type name for the proto3 "Any" type.
|
||||
constexpr absl::string_view kGoogleProtobufAny = "google.protobuf.Any";
|
||||
|
||||
// Converts a FieldDescriptor::Type to the corresponding FieldType.
|
||||
FieldType AsFieldType(proto_ns::FieldDescriptorProto::Type type) {
|
||||
return static_cast<FieldType>(type);
|
||||
@@ -81,7 +86,7 @@ absl::Status WriteValue(const FieldData& value, FieldType field_type,
|
||||
return absl::UnimplementedError(
|
||||
absl::StrCat("Cannot write type: ", field_type));
|
||||
}
|
||||
return mediapipe::OkStatus();
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// Serializes a packet value.
|
||||
@@ -167,6 +172,7 @@ absl::Status ReadValue(absl::string_view field_bytes, FieldType field_type,
|
||||
// Deserializes a packet from a protobuf field.
|
||||
absl::Status ReadField(absl::string_view bytes, const FieldDescriptor* field,
|
||||
FieldData* result) {
|
||||
RET_CHECK_NE(field, nullptr);
|
||||
FieldType field_type = AsFieldType(field->type());
|
||||
std::string message_type = (field_type == WireFormatLite::TYPE_MESSAGE)
|
||||
? field->message_type()->full_name()
|
||||
@@ -174,47 +180,137 @@ absl::Status ReadField(absl::string_view bytes, const FieldDescriptor* field,
|
||||
return ReadValue(bytes, field_type, message_type, result);
|
||||
}
|
||||
|
||||
// Converts a chain of fields and indexes into field-numbers and indexes.
|
||||
ProtoUtilLite::ProtoPath AsProtoPath(const FieldPath& field_path) {
|
||||
ProtoUtilLite::ProtoPath result;
|
||||
for (auto field : field_path) {
|
||||
result.push_back({field.first->number(), field.second});
|
||||
// Reads all values from a repeated field.
|
||||
absl::Status GetFieldValues(const FieldData& message_data,
|
||||
const FieldDescriptor& field,
|
||||
std::vector<FieldData>* result) {
|
||||
const std::string& message_bytes = message_data.message_value().value();
|
||||
FieldType field_type = AsFieldType(field.type());
|
||||
ProtoUtilLite proto_util;
|
||||
ProtoUtilLite::ProtoPath proto_path = {{field.number(), 0}};
|
||||
int count;
|
||||
MP_RETURN_IF_ERROR(
|
||||
proto_util.GetFieldCount(message_bytes, proto_path, field_type, &count));
|
||||
std::vector<std::string> field_values;
|
||||
MP_RETURN_IF_ERROR(proto_util.GetFieldRange(message_bytes, proto_path, count,
|
||||
field_type, &field_values));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
FieldData r;
|
||||
MP_RETURN_IF_ERROR(ReadField(field_values[i], &field, &r));
|
||||
result->push_back(std::move(r));
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// Reads one value from a field.
|
||||
absl::Status GetFieldValue(const FieldData& message_data,
|
||||
const FieldPathEntry& entry, FieldData* result) {
|
||||
RET_CHECK_NE(entry.field, nullptr);
|
||||
const std::string& message_bytes = message_data.message_value().value();
|
||||
FieldType field_type = AsFieldType(entry.field->type());
|
||||
ProtoUtilLite proto_util;
|
||||
ProtoUtilLite::ProtoPath proto_path = {{entry.field->number(), entry.index}};
|
||||
std::vector<std::string> field_values;
|
||||
MP_RETURN_IF_ERROR(proto_util.GetFieldRange(message_bytes, proto_path, 1,
|
||||
field_type, &field_values));
|
||||
MP_RETURN_IF_ERROR(ReadField(field_values[0], entry.field, result));
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// Writes one value to a field.
|
||||
absl::Status SetFieldValue(const FieldPathEntry& entry, const FieldData& value,
|
||||
FieldData* result) {
|
||||
std::vector<FieldData> field_values;
|
||||
ProtoUtilLite proto_util;
|
||||
FieldType field_type = AsFieldType(entry.field->type());
|
||||
ProtoUtilLite::ProtoPath proto_path = {{entry.field->number(), entry.index}};
|
||||
std::string* message_bytes = result->mutable_message_value()->mutable_value();
|
||||
int field_count;
|
||||
MP_RETURN_IF_ERROR(proto_util.GetFieldCount(*message_bytes, proto_path,
|
||||
field_type, &field_count));
|
||||
if (entry.index > field_count) {
|
||||
return absl::OutOfRangeError(
|
||||
absl::StrCat("Option field index out of range: ", entry.index));
|
||||
}
|
||||
int replace_length = entry.index < field_count ? 1 : 0;
|
||||
std::string field_value;
|
||||
MP_RETURN_IF_ERROR(WriteField(value, entry.field, &field_value));
|
||||
MP_RETURN_IF_ERROR(proto_util.ReplaceFieldRange(
|
||||
message_bytes, proto_path, replace_length, field_type, {field_value}));
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// Returns true for a field of type "google.protobuf.Any".
|
||||
bool IsProtobufAny(const FieldDescriptor* field) {
|
||||
return AsFieldType(field->type()) == FieldType::TYPE_MESSAGE &&
|
||||
field->message_type()->full_name() == kGoogleProtobufAny;
|
||||
}
|
||||
|
||||
// Returns the message FieldData from a serialized protobuf.Any.
|
||||
FieldData ParseProtobufAny(const FieldData& data) {
|
||||
protobuf::Any any;
|
||||
any.ParseFromString(data.message_value().value());
|
||||
FieldData result;
|
||||
result.mutable_message_value()->set_value(std::string(any.value()));
|
||||
result.mutable_message_value()->set_type_url(any.type_url());
|
||||
return result;
|
||||
}
|
||||
|
||||
// Returns the options protobuf for a subgraph.
|
||||
// TODO: Ensure that this works with multiple options protobufs.
|
||||
absl::Status GetOptionsMessage(
|
||||
const proto_ns::RepeatedPtrField<mediapipe::protobuf::Any>& options_any,
|
||||
const proto_ns::MessageLite& options_ext, FieldData* result) {
|
||||
// Read the "graph_options" or "node_options" field.
|
||||
for (const auto& options : options_any) {
|
||||
if (options.type_url().empty()) {
|
||||
continue;
|
||||
}
|
||||
result->mutable_message_value()->set_type_url(options.type_url());
|
||||
result->mutable_message_value()->set_value(std::string(options.value()));
|
||||
return mediapipe::OkStatus();
|
||||
}
|
||||
// Returns the serialized protobuf.Any containing a message FieldData.
|
||||
FieldData SerializeProtobufAny(const FieldData& data) {
|
||||
protobuf::Any any;
|
||||
any.set_value(data.message_value().value());
|
||||
any.set_type_url(data.message_value().type_url());
|
||||
FieldData result;
|
||||
result.mutable_message_value()->set_value(any.SerializeAsString());
|
||||
result.mutable_message_value()->set_type_url(TypeUrl(kGoogleProtobufAny));
|
||||
return result;
|
||||
}
|
||||
|
||||
// Read the "options" field.
|
||||
FieldData message_data;
|
||||
*message_data.mutable_message_value()->mutable_value() =
|
||||
options_ext.SerializeAsString();
|
||||
message_data.mutable_message_value()->set_type_url(options_ext.GetTypeName());
|
||||
std::vector<const FieldDescriptor*> ext_fields;
|
||||
OptionsRegistry::FindAllExtensions(options_ext.GetTypeName(), &ext_fields);
|
||||
for (auto ext_field : ext_fields) {
|
||||
absl::Status status = GetField({{ext_field, 0}}, message_data, result);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (result->has_message_value()) {
|
||||
return status;
|
||||
// Returns the field index of an extension type in a repeated field.
|
||||
StatusOr<int> FindExtensionIndex(const FieldData& message_data,
|
||||
FieldPathEntry* entry) {
|
||||
if (entry->field == nullptr || !IsProtobufAny(entry->field)) {
|
||||
return -1;
|
||||
}
|
||||
std::string& extension_type = entry->extension_type;
|
||||
std::vector<FieldData> field_values;
|
||||
RET_CHECK_NE(entry->field, nullptr);
|
||||
MP_RETURN_IF_ERROR(
|
||||
GetFieldValues(message_data, *entry->field, &field_values));
|
||||
for (int i = 0; i < field_values.size(); ++i) {
|
||||
FieldData extension = ParseProtobufAny(field_values[i]);
|
||||
if (extension_type == "*" ||
|
||||
ParseTypeUrl(extension.message_value().type_url()) == extension_type) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return mediapipe::OkStatus();
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Returns true if the value of a field is available.
|
||||
bool HasField(const FieldPath& field_path, const FieldData& message_data) {
|
||||
FieldData value;
|
||||
return GetField(field_path, message_data, &value).ok() &&
|
||||
value.value_case() != mediapipe::FieldData::VALUE_NOT_SET;
|
||||
}
|
||||
|
||||
// Returns the extension field containing the specified extension-type.
|
||||
const FieldDescriptor* FindExtensionField(const FieldData& message_data,
|
||||
absl::string_view extension_type) {
|
||||
std::string message_type =
|
||||
ParseTypeUrl(message_data.message_value().type_url());
|
||||
std::vector<const FieldDescriptor*> extensions;
|
||||
OptionsRegistry::FindAllExtensions(message_type, &extensions);
|
||||
for (const FieldDescriptor* extension : extensions) {
|
||||
if (extension->message_type()->full_name() == extension_type) {
|
||||
return extension;
|
||||
}
|
||||
if (extension_type == "*" && HasField({{extension, 0}}, message_data)) {
|
||||
return extension;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Sets a protobuf in a repeated protobuf::Any field.
|
||||
@@ -234,6 +330,20 @@ void SetOptionsMessage(
|
||||
*options_any->mutable_value() = node_options.message_value().value();
|
||||
}
|
||||
|
||||
// Returns the count of values in a repeated field.
|
||||
int FieldCount(const FieldData& message_data, const FieldDescriptor* field) {
|
||||
const std::string& message_bytes = message_data.message_value().value();
|
||||
FieldType field_type = AsFieldType(field->type());
|
||||
ProtoUtilLite proto_util;
|
||||
ProtoUtilLite::ProtoPath proto_path = {{field->number(), 0}};
|
||||
int count;
|
||||
if (proto_util.GetFieldCount(message_bytes, proto_path, field_type, &count)
|
||||
.ok()) {
|
||||
return count;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// Deserializes a packet containing a MessageLite value.
|
||||
@@ -247,8 +357,8 @@ absl::Status ReadMessage(const std::string& value, const std::string& type_name,
|
||||
}
|
||||
|
||||
// Merge two options FieldData values.
|
||||
absl::Status MergeOptionsMessages(const FieldData& base, const FieldData& over,
|
||||
FieldData* result) {
|
||||
absl::Status MergeMessages(const FieldData& base, const FieldData& over,
|
||||
FieldData* result) {
|
||||
absl::Status status;
|
||||
if (over.value_case() == FieldData::VALUE_NOT_SET) {
|
||||
*result = base;
|
||||
@@ -278,28 +388,148 @@ absl::Status MergeOptionsMessages(const FieldData& base, const FieldData& over,
|
||||
return status;
|
||||
}
|
||||
|
||||
// Returns either the extension field or the repeated protobuf.Any field index
|
||||
// holding the specified extension-type.
|
||||
absl::Status FindExtension(const FieldData& message_data,
|
||||
FieldPathEntry* entry) {
|
||||
if (entry->extension_type.empty()) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// For repeated protobuf::Any, find the index for the extension_type.
|
||||
ASSIGN_OR_RETURN(int index, FindExtensionIndex(message_data, entry));
|
||||
if (index != -1) {
|
||||
entry->index = index;
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// Returns the extension field containing the specified extension-type.
|
||||
std::string& extension_type = entry->extension_type;
|
||||
const FieldDescriptor* field =
|
||||
FindExtensionField(message_data, extension_type);
|
||||
if (field != nullptr) {
|
||||
entry->field = field;
|
||||
entry->index = 0;
|
||||
return absl::OkStatus();
|
||||
}
|
||||
return absl::NotFoundError(
|
||||
absl::StrCat("Option extension not found: ", extension_type));
|
||||
}
|
||||
|
||||
// Return the FieldPath referencing an extension message.
|
||||
FieldPath GetExtensionPath(const std::string& parent_type,
|
||||
const std::string& extension_type,
|
||||
const std::string& field_name,
|
||||
bool is_protobuf_any) {
|
||||
FieldPath result;
|
||||
const tool::Descriptor* parent_descriptor =
|
||||
tool::OptionsRegistry::GetProtobufDescriptor(parent_type);
|
||||
FieldPathEntry field_entry;
|
||||
field_entry.field = parent_descriptor->FindFieldByName(field_name);
|
||||
if (is_protobuf_any) {
|
||||
field_entry.extension_type = extension_type;
|
||||
result = {std::move(field_entry)};
|
||||
} else {
|
||||
field_entry.index = 0;
|
||||
FieldPathEntry extension_entry;
|
||||
extension_entry.extension_type = extension_type;
|
||||
result = {std::move(field_entry), std::move(extension_entry)};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Returns the requested options protobuf for a graph node.
|
||||
absl::Status GetNodeOptions(const FieldData& message_data,
|
||||
const std::string& extension_type,
|
||||
FieldData* result) {
|
||||
constexpr char kOptionsName[] = "options";
|
||||
constexpr char kNodeOptionsName[] = "node_options";
|
||||
std::string parent_type = options_field_util::ParseTypeUrl(
|
||||
std::string(message_data.message_value().type_url()));
|
||||
FieldPath path;
|
||||
Status status;
|
||||
path = GetExtensionPath(parent_type, extension_type, kOptionsName, false);
|
||||
status = GetField(path, message_data, result);
|
||||
if (status.ok()) {
|
||||
return status;
|
||||
}
|
||||
path = GetExtensionPath(parent_type, extension_type, kNodeOptionsName, true);
|
||||
status = GetField(path, message_data, result);
|
||||
return status;
|
||||
}
|
||||
|
||||
// Returns the requested options protobuf for a graph.
|
||||
absl::Status GetGraphOptions(const FieldData& message_data,
|
||||
const std::string& extension_type,
|
||||
FieldData* result) {
|
||||
constexpr char kOptionsName[] = "options";
|
||||
constexpr char kGraphOptionsName[] = "graph_options";
|
||||
std::string parent_type = options_field_util::ParseTypeUrl(
|
||||
std::string(message_data.message_value().type_url()));
|
||||
FieldPath path;
|
||||
Status status;
|
||||
path = GetExtensionPath(parent_type, extension_type, kOptionsName, false);
|
||||
status = GetField(path, message_data, result);
|
||||
if (status.ok()) {
|
||||
return status;
|
||||
}
|
||||
path = GetExtensionPath(parent_type, extension_type, kGraphOptionsName, true);
|
||||
status = GetField(path, message_data, result);
|
||||
return status;
|
||||
}
|
||||
|
||||
// Reads a FieldData value from a protobuf field.
|
||||
absl::Status GetField(const FieldPath& field_path,
|
||||
const FieldData& message_data, FieldData* result) {
|
||||
if (field_path.empty()) {
|
||||
*result->mutable_message_value() = message_data.message_value();
|
||||
return absl::OkStatus();
|
||||
}
|
||||
FieldPathEntry head = field_path.front();
|
||||
FieldPath tail = field_path;
|
||||
tail.erase(tail.begin());
|
||||
if (!head.extension_type.empty()) {
|
||||
MP_RETURN_IF_ERROR(FindExtension(message_data, &head));
|
||||
}
|
||||
if (tail.empty() && FieldCount(message_data, head.field) == 0) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
MP_RETURN_IF_ERROR(GetFieldValue(message_data, head, result));
|
||||
if (IsProtobufAny(head.field)) {
|
||||
*result = ParseProtobufAny(*result);
|
||||
}
|
||||
if (!tail.empty()) {
|
||||
FieldData child = *result;
|
||||
MP_RETURN_IF_ERROR(GetField(tail, child, result));
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// Writes a FieldData value into protobuf field.
|
||||
absl::Status SetField(const FieldPath& field_path, const FieldData& value,
|
||||
FieldData* message_data) {
|
||||
if (field_path.empty()) {
|
||||
*message_data->mutable_message_value() = value.message_value();
|
||||
return mediapipe::OkStatus();
|
||||
return absl::OkStatus();
|
||||
}
|
||||
ProtoUtilLite proto_util;
|
||||
const FieldDescriptor* field = field_path.back().first;
|
||||
FieldType field_type = AsFieldType(field->type());
|
||||
std::string field_value;
|
||||
MP_RETURN_IF_ERROR(WriteField(value, field, &field_value));
|
||||
ProtoUtilLite::ProtoPath proto_path = AsProtoPath(field_path);
|
||||
std::string* message_bytes =
|
||||
message_data->mutable_message_value()->mutable_value();
|
||||
int field_count;
|
||||
MP_RETURN_IF_ERROR(proto_util.GetFieldCount(*message_bytes, proto_path,
|
||||
field_type, &field_count));
|
||||
MP_RETURN_IF_ERROR(
|
||||
proto_util.ReplaceFieldRange(message_bytes, AsProtoPath(field_path),
|
||||
field_count, field_type, {field_value}));
|
||||
return mediapipe::OkStatus();
|
||||
FieldPathEntry head = field_path.front();
|
||||
FieldPath tail = field_path;
|
||||
tail.erase(tail.begin());
|
||||
if (!head.extension_type.empty()) {
|
||||
MP_RETURN_IF_ERROR(FindExtension(*message_data, &head));
|
||||
}
|
||||
if (tail.empty()) {
|
||||
MP_RETURN_IF_ERROR(SetFieldValue(head, value, message_data));
|
||||
} else {
|
||||
FieldData child;
|
||||
MP_RETURN_IF_ERROR(GetFieldValue(*message_data, head, &child));
|
||||
MP_RETURN_IF_ERROR(SetField(tail, value, &child));
|
||||
if (IsProtobufAny(head.field)) {
|
||||
child = SerializeProtobufAny(child);
|
||||
}
|
||||
MP_RETURN_IF_ERROR(SetFieldValue(head, child, message_data));
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
// Merges a packet value into nested protobuf Message.
|
||||
@@ -308,7 +538,7 @@ absl::Status MergeField(const FieldPath& field_path, const FieldData& value,
|
||||
absl::Status status;
|
||||
FieldType field_type = field_path.empty()
|
||||
? FieldType::TYPE_MESSAGE
|
||||
: AsFieldType(field_path.back().first->type());
|
||||
: AsFieldType(field_path.back().field->type());
|
||||
std::string message_type =
|
||||
(value.has_message_value())
|
||||
? ParseTypeUrl(std::string(value.message_value().type_url()))
|
||||
@@ -317,49 +547,12 @@ absl::Status MergeField(const FieldPath& field_path, const FieldData& value,
|
||||
if (field_type == FieldType::TYPE_MESSAGE) {
|
||||
FieldData b;
|
||||
status.Update(GetField(field_path, *message_data, &b));
|
||||
status.Update(MergeOptionsMessages(b, v, &v));
|
||||
status.Update(MergeMessages(b, v, &v));
|
||||
}
|
||||
status.Update(SetField(field_path, v, message_data));
|
||||
return status;
|
||||
}
|
||||
|
||||
// Reads a packet value from a protobuf field.
|
||||
absl::Status GetField(const FieldPath& field_path,
|
||||
const FieldData& message_data, FieldData* result) {
|
||||
if (field_path.empty()) {
|
||||
*result->mutable_message_value() = message_data.message_value();
|
||||
return mediapipe::OkStatus();
|
||||
}
|
||||
ProtoUtilLite proto_util;
|
||||
const FieldDescriptor* field = field_path.back().first;
|
||||
FieldType field_type = AsFieldType(field->type());
|
||||
std::vector<std::string> field_values;
|
||||
ProtoUtilLite::ProtoPath proto_path = AsProtoPath(field_path);
|
||||
const std::string& message_bytes = message_data.message_value().value();
|
||||
int field_count;
|
||||
MP_RETURN_IF_ERROR(proto_util.GetFieldCount(message_bytes, proto_path,
|
||||
field_type, &field_count));
|
||||
if (field_count == 0) {
|
||||
return mediapipe::OkStatus();
|
||||
}
|
||||
MP_RETURN_IF_ERROR(proto_util.GetFieldRange(message_bytes, proto_path, 1,
|
||||
field_type, &field_values));
|
||||
MP_RETURN_IF_ERROR(ReadField(field_values.front(), field, result));
|
||||
return mediapipe::OkStatus();
|
||||
}
|
||||
|
||||
// Returns the options protobuf for a graph.
|
||||
absl::Status GetOptionsMessage(const CalculatorGraphConfig& config,
|
||||
FieldData* result) {
|
||||
return GetOptionsMessage(config.graph_options(), config.options(), result);
|
||||
}
|
||||
|
||||
// Returns the options protobuf for a node.
|
||||
absl::Status GetOptionsMessage(const CalculatorGraphConfig::Node& node,
|
||||
FieldData* result) {
|
||||
return GetOptionsMessage(node.node_options(), node.options(), result);
|
||||
}
|
||||
|
||||
// Sets the node_options field in a Node, and clears the options field.
|
||||
void SetOptionsMessage(const FieldData& node_options,
|
||||
CalculatorGraphConfig::Node* node) {
|
||||
@@ -367,6 +560,16 @@ void SetOptionsMessage(const FieldData& node_options,
|
||||
node->clear_options();
|
||||
}
|
||||
|
||||
// Serialize a MessageLite to a FieldData.
|
||||
FieldData AsFieldData(const proto_ns::MessageLite& message) {
|
||||
FieldData result;
|
||||
*result.mutable_message_value()->mutable_value() =
|
||||
message.SerializePartialAsString();
|
||||
*result.mutable_message_value()->mutable_type_url() =
|
||||
TypeUrl(message.GetTypeName());
|
||||
return result;
|
||||
}
|
||||
|
||||
// Represents a protobuf enum value stored in a Packet.
|
||||
struct ProtoEnum {
|
||||
ProtoEnum(int32 v) : value(v) {}
|
||||
@@ -415,7 +618,7 @@ absl::Status AsPacket(const FieldData& data, Packet* result) {
|
||||
case FieldData::VALUE_NOT_SET:
|
||||
*result = Packet();
|
||||
}
|
||||
return mediapipe::OkStatus();
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status AsFieldData(Packet packet, FieldData* result) {
|
||||
@@ -436,7 +639,7 @@ absl::Status AsFieldData(Packet packet, FieldData* result) {
|
||||
packet.GetProtoMessageLite().SerializeAsString());
|
||||
result->mutable_message_value()->set_type_url(
|
||||
TypeUrl(packet.GetProtoMessageLite().GetTypeName()));
|
||||
return mediapipe::OkStatus();
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
if (kTypeIds->count(packet.GetTypeId()) == 0) {
|
||||
@@ -473,7 +676,7 @@ absl::Status AsFieldData(Packet packet, FieldData* result) {
|
||||
result->set_string_value(packet.Get<std::string>());
|
||||
break;
|
||||
}
|
||||
return mediapipe::OkStatus();
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
std::string TypeUrl(absl::string_view type_name) {
|
||||
|
||||
@@ -19,8 +19,15 @@ namespace tool {
|
||||
// Utility to read and write Packet data from protobuf fields.
|
||||
namespace options_field_util {
|
||||
|
||||
// A chain of nested fields and indexes.
|
||||
using FieldPath = std::vector<std::pair<const FieldDescriptor*, int>>;
|
||||
// A protobuf field and index description.
|
||||
struct FieldPathEntry {
|
||||
const FieldDescriptor* field = nullptr;
|
||||
int index = -1;
|
||||
std::string extension_type;
|
||||
};
|
||||
|
||||
// A chain of nested protobuf fields and indexes.
|
||||
using FieldPath = std::vector<FieldPathEntry>;
|
||||
|
||||
// Writes a field value into protobuf field.
|
||||
absl::Status SetField(const FieldPath& field_path, const FieldData& value,
|
||||
@@ -39,21 +46,26 @@ absl::Status ReadMessage(const std::string& value, const std::string& type_name,
|
||||
Packet* result);
|
||||
|
||||
// Merge two options protobuf field values.
|
||||
absl::Status MergeOptionsMessages(const FieldData& base, const FieldData& over,
|
||||
FieldData* result);
|
||||
absl::Status MergeMessages(const FieldData& base, const FieldData& over,
|
||||
FieldData* result);
|
||||
|
||||
// Returns the options protobuf for a graph.
|
||||
absl::Status GetOptionsMessage(const CalculatorGraphConfig& config,
|
||||
FieldData* result);
|
||||
// Returns the requested options protobuf for a graph.
|
||||
absl::Status GetNodeOptions(const FieldData& message_data,
|
||||
const std::string& extension_type,
|
||||
FieldData* result);
|
||||
|
||||
// Returns the options protobuf for a node.
|
||||
absl::Status GetOptionsMessage(const CalculatorGraphConfig::Node& node,
|
||||
FieldData* result);
|
||||
// Returns the requested options protobuf for a graph node.
|
||||
absl::Status GetGraphOptions(const FieldData& message_data,
|
||||
const std::string& extension_type,
|
||||
FieldData* result);
|
||||
|
||||
// Sets the node_options field in a Node, and clears the options field.
|
||||
void SetOptionsMessage(const FieldData& node_options,
|
||||
CalculatorGraphConfig::Node* node);
|
||||
|
||||
// Serialize a MessageLite to a FieldData.
|
||||
FieldData AsFieldData(const proto_ns::MessageLite& message);
|
||||
|
||||
// Constructs a Packet for a FieldData proto.
|
||||
absl::Status AsPacket(const FieldData& data, Packet* result);
|
||||
|
||||
|
||||
@@ -5,17 +5,42 @@
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/strings/match.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "mediapipe/framework/packet.h"
|
||||
#include "mediapipe/framework/packet_type.h"
|
||||
#include "mediapipe/framework/port/advanced_proto_inc.h"
|
||||
#include "mediapipe/framework/port/any_proto.h"
|
||||
#include "mediapipe/framework/port/status.h"
|
||||
#include "mediapipe/framework/tool/name_util.h"
|
||||
#include "mediapipe/framework/tool/options_registry.h"
|
||||
|
||||
namespace mediapipe {
|
||||
namespace tool {
|
||||
|
||||
namespace {
|
||||
|
||||
// StrSplit Delimiter to split strings at single colon tokens, ignoring
|
||||
// double-colon tokens.
|
||||
class SingleColonDelimiter {
|
||||
public:
|
||||
SingleColonDelimiter() {}
|
||||
absl::string_view Find(absl::string_view text, size_t pos) const {
|
||||
while (pos < text.length()) {
|
||||
size_t p = text.find(':', pos);
|
||||
p = (p == absl::string_view::npos) ? text.length() : p;
|
||||
if (p >= text.length() - 1 || text[p + 1] != ':') {
|
||||
return text.substr(p, 1);
|
||||
}
|
||||
pos = p + 2;
|
||||
}
|
||||
return text.substr(text.length(), 0);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// Helper functions for parsing the graph options syntax.
|
||||
class OptionsSyntaxUtil::OptionsSyntaxHelper {
|
||||
public:
|
||||
@@ -31,13 +56,32 @@ class OptionsSyntaxUtil::OptionsSyntaxHelper {
|
||||
// Returns the option protobuf field name for a tag or packet name.
|
||||
absl::string_view OptionFieldName(absl::string_view name) { return name; }
|
||||
|
||||
// Return the extension-type specified for an option field.
|
||||
absl::string_view ExtensionType(absl::string_view option_name) {
|
||||
constexpr absl::string_view kExt = "Ext::";
|
||||
if (absl::StartsWithIgnoreCase(option_name, kExt)) {
|
||||
return option_name.substr(kExt.size());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Returns the field names encoded in an options tag.
|
||||
std::vector<absl::string_view> OptionTagNames(absl::string_view tag) {
|
||||
if (absl::StartsWith(tag, syntax_.tag_name)) {
|
||||
tag = tag.substr(syntax_.tag_name.length());
|
||||
} else if (absl::StartsWith(tag, syntax_.packet_name)) {
|
||||
tag = tag.substr(syntax_.packet_name.length());
|
||||
}
|
||||
if (absl::StartsWith(tag, syntax_.separator)) {
|
||||
tag = tag.substr(syntax_.separator.length());
|
||||
}
|
||||
return absl::StrSplit(tag, syntax_.separator);
|
||||
}
|
||||
|
||||
// Returns the field-path for an option stream-tag.
|
||||
FieldPath OptionFieldPath(const std::string& tag,
|
||||
FieldPath OptionFieldPath(absl::string_view tag,
|
||||
const Descriptor* descriptor) {
|
||||
int prefix = syntax_.tag_name.length() + syntax_.separator.length();
|
||||
std::string suffix = tag.substr(prefix);
|
||||
std::vector<absl::string_view> name_tags =
|
||||
absl::StrSplit(suffix, syntax_.separator);
|
||||
std::vector<absl::string_view> name_tags = OptionTagNames(tag);
|
||||
FieldPath result;
|
||||
for (absl::string_view name_tag : name_tags) {
|
||||
if (name_tag.empty()) {
|
||||
@@ -46,8 +90,16 @@ class OptionsSyntaxUtil::OptionsSyntaxHelper {
|
||||
absl::string_view option_name = OptionFieldName(name_tag);
|
||||
int index;
|
||||
if (absl::SimpleAtoi(option_name, &index)) {
|
||||
result.back().second = index;
|
||||
result.back().index = index;
|
||||
}
|
||||
if (!ExtensionType(option_name).empty()) {
|
||||
std::string extension_type = std::string(ExtensionType(option_name));
|
||||
result.push_back({nullptr, 0, extension_type});
|
||||
descriptor = OptionsRegistry::GetProtobufDescriptor(extension_type);
|
||||
} else {
|
||||
if (descriptor == nullptr) {
|
||||
break;
|
||||
}
|
||||
auto field = descriptor->FindFieldByName(std::string(option_name));
|
||||
descriptor = field ? field->message_type() : nullptr;
|
||||
result.push_back({std::move(field), 0});
|
||||
@@ -78,7 +130,7 @@ class OptionsSyntaxUtil::OptionsSyntaxHelper {
|
||||
}
|
||||
|
||||
// Converts slash-separated field names into a tag name.
|
||||
std::string OptionFieldsTag(const std::string& option_names) {
|
||||
std::string OptionFieldsTag(absl::string_view option_names) {
|
||||
std::string tag_prefix = syntax_.tag_name + syntax_.separator;
|
||||
std::vector<absl::string_view> names = absl::StrSplit(option_names, '/');
|
||||
if (!names.empty() && names[0] == syntax_.tag_name) {
|
||||
@@ -129,15 +181,18 @@ OptionsSyntaxUtil::OptionsSyntaxUtil(const std::string& tag_name,
|
||||
|
||||
OptionsSyntaxUtil::~OptionsSyntaxUtil() {}
|
||||
|
||||
std::string OptionsSyntaxUtil::OptionFieldsTag(
|
||||
const std::string& option_names) {
|
||||
std::string OptionsSyntaxUtil::OptionFieldsTag(absl::string_view option_names) {
|
||||
return syntax_helper_->OptionFieldsTag(option_names);
|
||||
}
|
||||
|
||||
OptionsSyntaxUtil::FieldPath OptionsSyntaxUtil::OptionFieldPath(
|
||||
const std::string& tag, const Descriptor* descriptor) {
|
||||
absl::string_view tag, const Descriptor* descriptor) {
|
||||
return syntax_helper_->OptionFieldPath(tag, descriptor);
|
||||
}
|
||||
std::vector<absl::string_view> OptionsSyntaxUtil::StrSplitTags(
|
||||
absl::string_view tag_and_name) {
|
||||
return absl::StrSplit(tag_and_name, SingleColonDelimiter());
|
||||
}
|
||||
|
||||
} // namespace tool
|
||||
} // namespace mediapipe
|
||||
|
||||
@@ -28,12 +28,15 @@ class OptionsSyntaxUtil {
|
||||
~OptionsSyntaxUtil();
|
||||
|
||||
// Converts slash-separated field names into a tag name.
|
||||
std::string OptionFieldsTag(const std::string& option_names);
|
||||
std::string OptionFieldsTag(absl::string_view option_names);
|
||||
|
||||
// Returns the field-path for an option stream-tag.
|
||||
FieldPath OptionFieldPath(const std::string& tag,
|
||||
FieldPath OptionFieldPath(absl::string_view tag,
|
||||
const Descriptor* descriptor);
|
||||
|
||||
// Splits a std::string into "tag" and "name" delimited by a single colon.
|
||||
std::vector<absl::string_view> StrSplitTags(absl::string_view tag_and_name);
|
||||
|
||||
private:
|
||||
class OptionsSyntaxHelper;
|
||||
std::unique_ptr<OptionsSyntaxHelper> syntax_helper_;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "absl/strings/ascii.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "mediapipe/framework/calculator_context.h"
|
||||
#include "mediapipe/framework/input_stream_shard.h"
|
||||
#include "mediapipe/framework/output_side_packet.h"
|
||||
@@ -24,50 +25,77 @@
|
||||
namespace mediapipe {
|
||||
namespace tool {
|
||||
|
||||
using options_field_util::FieldPath;
|
||||
using options_field_util::GetField;
|
||||
using options_field_util::GetGraphOptions;
|
||||
using options_field_util::GetNodeOptions;
|
||||
using options_field_util::MergeField;
|
||||
using options_field_util::MergeMessages;
|
||||
|
||||
// Returns the type for the root options message if specified.
|
||||
std::string ExtensionType(const std::string& option_fields_tag) {
|
||||
OptionsSyntaxUtil syntax_util;
|
||||
options_field_util::FieldPath field_path =
|
||||
syntax_util.OptionFieldPath(option_fields_tag, nullptr);
|
||||
std::string result = !field_path.empty() ? field_path[0].extension_type : "";
|
||||
return !result.empty() ? result : "*";
|
||||
}
|
||||
|
||||
// Constructs a FieldPath for field names starting at a message type.
|
||||
FieldPath GetPath(const std::string& path_tag,
|
||||
const std::string& message_type) {
|
||||
OptionsSyntaxUtil syntax_util;
|
||||
const Descriptor* descriptor =
|
||||
OptionsRegistry::GetProtobufDescriptor(message_type);
|
||||
return syntax_util.OptionFieldPath(path_tag, descriptor);
|
||||
}
|
||||
|
||||
// Returns the message type for a FieldData.
|
||||
std::string MessageType(FieldData message) {
|
||||
return options_field_util::ParseTypeUrl(
|
||||
std::string(message.message_value().type_url()));
|
||||
}
|
||||
|
||||
// Copy literal options from graph_options to node_options.
|
||||
absl::Status CopyLiteralOptions(CalculatorGraphConfig::Node parent_node,
|
||||
CalculatorGraphConfig* config) {
|
||||
Status status;
|
||||
FieldData config_options, parent_node_options, graph_options;
|
||||
status.Update(
|
||||
options_field_util::GetOptionsMessage(*config, &config_options));
|
||||
status.Update(
|
||||
options_field_util::GetOptionsMessage(parent_node, &parent_node_options));
|
||||
status.Update(options_field_util::MergeOptionsMessages(
|
||||
config_options, parent_node_options, &graph_options));
|
||||
const Descriptor* options_descriptor =
|
||||
OptionsRegistry::GetProtobufDescriptor(options_field_util::ParseTypeUrl(
|
||||
std::string(graph_options.message_value().type_url())));
|
||||
if (!options_descriptor) {
|
||||
return status;
|
||||
}
|
||||
FieldData graph_data = options_field_util::AsFieldData(*config);
|
||||
FieldData parent_data = options_field_util::AsFieldData(parent_node);
|
||||
|
||||
OptionsSyntaxUtil syntax_util;
|
||||
for (auto& node : *config->mutable_node()) {
|
||||
FieldData node_data;
|
||||
status.Update(options_field_util::GetOptionsMessage(node, &node_data));
|
||||
if (!node_data.has_message_value() || node.option_value_size() == 0) {
|
||||
continue;
|
||||
}
|
||||
const Descriptor* node_options_descriptor =
|
||||
OptionsRegistry::GetProtobufDescriptor(options_field_util::ParseTypeUrl(
|
||||
std::string(node_data.message_value().type_url())));
|
||||
if (!node_options_descriptor) {
|
||||
continue;
|
||||
}
|
||||
FieldData node_data = options_field_util::AsFieldData(node);
|
||||
|
||||
for (const std::string& option_def : node.option_value()) {
|
||||
std::vector<std::string> tag_and_name = absl::StrSplit(option_def, ':');
|
||||
std::vector<absl::string_view> tag_and_name =
|
||||
syntax_util.StrSplitTags(option_def);
|
||||
std::string graph_tag = syntax_util.OptionFieldsTag(tag_and_name[1]);
|
||||
std::string graph_extension_type = ExtensionType(graph_tag);
|
||||
std::string node_tag = syntax_util.OptionFieldsTag(tag_and_name[0]);
|
||||
std::string node_extension_type = ExtensionType(node_tag);
|
||||
FieldData graph_options;
|
||||
GetGraphOptions(graph_data, graph_extension_type, &graph_options)
|
||||
.IgnoreError();
|
||||
FieldData parent_options;
|
||||
GetNodeOptions(parent_data, graph_extension_type, &parent_options)
|
||||
.IgnoreError();
|
||||
status.Update(
|
||||
MergeMessages(graph_options, parent_options, &graph_options));
|
||||
FieldData node_options;
|
||||
status.Update(
|
||||
GetNodeOptions(node_data, node_extension_type, &node_options));
|
||||
if (!node_options.has_message_value() ||
|
||||
!graph_options.has_message_value()) {
|
||||
continue;
|
||||
}
|
||||
FieldPath graph_path = GetPath(graph_tag, MessageType(graph_options));
|
||||
FieldPath node_path = GetPath(node_tag, MessageType(node_options));
|
||||
FieldData packet_data;
|
||||
status.Update(options_field_util::GetField(
|
||||
syntax_util.OptionFieldPath(graph_tag, options_descriptor),
|
||||
graph_options, &packet_data));
|
||||
status.Update(options_field_util::MergeField(
|
||||
syntax_util.OptionFieldPath(node_tag, node_options_descriptor),
|
||||
packet_data, &node_data));
|
||||
status.Update(GetField(graph_path, graph_options, &packet_data));
|
||||
status.Update(MergeField(node_path, packet_data, &node_options));
|
||||
options_field_util::SetOptionsMessage(node_options, &node);
|
||||
}
|
||||
options_field_util::SetOptionsMessage(node_data, &node);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "mediapipe/framework/calculator_framework.h"
|
||||
#include "mediapipe/framework/deps/message_matchers.h"
|
||||
#include "mediapipe/framework/port/gtest.h"
|
||||
@@ -22,6 +23,7 @@
|
||||
#include "mediapipe/framework/port/status_matchers.h"
|
||||
#include "mediapipe/framework/testdata/night_light_calculator.pb.h"
|
||||
#include "mediapipe/framework/tool/node_chain_subgraph.pb.h"
|
||||
#include "mediapipe/framework/tool/options_field_util.h"
|
||||
#include "mediapipe/framework/tool/options_registry.h"
|
||||
#include "mediapipe/framework/tool/options_syntax_util.h"
|
||||
|
||||
@@ -51,6 +53,35 @@ class NightLightCalculator : public CalculatorBase {
|
||||
};
|
||||
REGISTER_CALCULATOR(NightLightCalculator);
|
||||
|
||||
using tool::options_field_util::FieldPath;
|
||||
|
||||
// Validates FieldPathEntry contents.
|
||||
bool Equals(const tool::options_field_util::FieldPathEntry& entry,
|
||||
const std::string& field_name, int index,
|
||||
const std::string& extension_type) {
|
||||
const std::string& name = entry.field ? entry.field->name() : "";
|
||||
return name == field_name && entry.index == index &&
|
||||
entry.extension_type == extension_type;
|
||||
}
|
||||
|
||||
// Serializes a MessageLite into FieldData.message_value.
|
||||
FieldData AsFieldData(const proto_ns::MessageLite& message) {
|
||||
FieldData result;
|
||||
*result.mutable_message_value()->mutable_value() =
|
||||
message.SerializeAsString();
|
||||
result.mutable_message_value()->set_type_url(message.GetTypeName());
|
||||
return result;
|
||||
}
|
||||
|
||||
// Returns the type for the root options message if specified.
|
||||
std::string ExtensionType(const std::string& option_fields_tag) {
|
||||
tool::OptionsSyntaxUtil syntax_util;
|
||||
tool::options_field_util::FieldPath field_path =
|
||||
syntax_util.OptionFieldPath(option_fields_tag, nullptr);
|
||||
std::string result = !field_path.empty() ? field_path[0].extension_type : "";
|
||||
return !result.empty() ? result : "*";
|
||||
}
|
||||
|
||||
// Tests for calculator and graph options.
|
||||
//
|
||||
class OptionsUtilTest : public ::testing::Test {
|
||||
@@ -150,8 +181,8 @@ TEST_F(OptionsUtilTest, OptionsSyntaxUtil) {
|
||||
EXPECT_EQ(tag, "OPTIONS/sub_options/num_lights");
|
||||
field_path = syntax_util.OptionFieldPath(tag, descriptor);
|
||||
EXPECT_EQ(field_path.size(), 2);
|
||||
EXPECT_EQ(field_path[0].first->name(), "sub_options");
|
||||
EXPECT_EQ(field_path[1].first->name(), "num_lights");
|
||||
EXPECT_EQ(field_path[0].field->name(), "sub_options");
|
||||
EXPECT_EQ(field_path[1].field->name(), "num_lights");
|
||||
}
|
||||
{
|
||||
// A tag syntax with a text-coded separator.
|
||||
@@ -160,10 +191,100 @@ TEST_F(OptionsUtilTest, OptionsSyntaxUtil) {
|
||||
EXPECT_EQ(tag, "OPTIONS_Z0Z_sub_options_Z0Z_num_lights");
|
||||
field_path = syntax_util.OptionFieldPath(tag, descriptor);
|
||||
EXPECT_EQ(field_path.size(), 2);
|
||||
EXPECT_EQ(field_path[0].first->name(), "sub_options");
|
||||
EXPECT_EQ(field_path[1].first->name(), "num_lights");
|
||||
EXPECT_EQ(field_path[0].field->name(), "sub_options");
|
||||
EXPECT_EQ(field_path[1].field->name(), "num_lights");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(OptionsUtilTest, OptionFieldPath) {
|
||||
tool::OptionsSyntaxUtil syntax_util;
|
||||
std::vector<absl::string_view> split;
|
||||
split = syntax_util.StrSplitTags("a/graph/option:a/node/option");
|
||||
EXPECT_EQ(2, split.size());
|
||||
EXPECT_EQ(split[0], "a/graph/option");
|
||||
EXPECT_EQ(split[1], "a/node/option");
|
||||
split = syntax_util.StrSplitTags("Ext::a/graph/option:Ext::a/node/option");
|
||||
EXPECT_EQ(2, split.size());
|
||||
EXPECT_EQ(split[0], "Ext::a/graph/option");
|
||||
EXPECT_EQ(split[1], "Ext::a/node/option");
|
||||
|
||||
split =
|
||||
syntax_util.StrSplitTags("chain_length:options/sub_options/num_lights");
|
||||
EXPECT_EQ(2, split.size());
|
||||
EXPECT_EQ(split[0], "chain_length");
|
||||
EXPECT_EQ(split[1], "options/sub_options/num_lights");
|
||||
const tool::Descriptor* descriptor =
|
||||
tool::OptionsRegistry::GetProtobufDescriptor(
|
||||
"mediapipe.NightLightCalculatorOptions");
|
||||
tool::options_field_util::FieldPath field_path =
|
||||
syntax_util.OptionFieldPath(split[1], descriptor);
|
||||
EXPECT_EQ(field_path.size(), 2);
|
||||
EXPECT_EQ(field_path[0].field->name(), "sub_options");
|
||||
EXPECT_EQ(field_path[1].field->name(), "num_lights");
|
||||
}
|
||||
|
||||
TEST_F(OptionsUtilTest, FindOptionsMessage) {
|
||||
tool::OptionsSyntaxUtil syntax_util;
|
||||
std::vector<absl::string_view> split;
|
||||
split =
|
||||
syntax_util.StrSplitTags("chain_length:options/sub_options/num_lights");
|
||||
EXPECT_EQ(2, split.size());
|
||||
EXPECT_EQ(split[0], "chain_length");
|
||||
EXPECT_EQ(split[1], "options/sub_options/num_lights");
|
||||
const tool::Descriptor* descriptor =
|
||||
tool::OptionsRegistry::GetProtobufDescriptor(
|
||||
"mediapipe.NightLightCalculatorOptions");
|
||||
tool::options_field_util::FieldPath field_path =
|
||||
syntax_util.OptionFieldPath(split[1], descriptor);
|
||||
EXPECT_EQ(field_path.size(), 2);
|
||||
EXPECT_TRUE(Equals(field_path[0], "sub_options", 0, ""));
|
||||
EXPECT_TRUE(Equals(field_path[1], "num_lights", 0, ""));
|
||||
|
||||
{
|
||||
// NightLightCalculatorOptions in Node.options.
|
||||
CalculatorGraphConfig::Node node;
|
||||
NightLightCalculatorOptions* options =
|
||||
node.mutable_options()->MutableExtension(
|
||||
NightLightCalculatorOptions::ext);
|
||||
options->mutable_sub_options()->add_num_lights(33);
|
||||
|
||||
// Retrieve the specified option.
|
||||
FieldData node_data = AsFieldData(node);
|
||||
auto path = field_path;
|
||||
std::string node_extension_type = ExtensionType(std::string(split[1]));
|
||||
FieldData node_options;
|
||||
MP_EXPECT_OK(tool::options_field_util::GetNodeOptions(
|
||||
node_data, node_extension_type, &node_options));
|
||||
FieldData packet_data;
|
||||
MP_EXPECT_OK(tool::options_field_util::GetField(field_path, node_options,
|
||||
&packet_data));
|
||||
EXPECT_EQ(packet_data.value_case(), FieldData::kInt32Value);
|
||||
EXPECT_EQ(packet_data.int32_value(), 33);
|
||||
}
|
||||
|
||||
{
|
||||
// NightLightCalculatorOptions in Node.node_options.
|
||||
CalculatorGraphConfig::Node node;
|
||||
NightLightCalculatorOptions options;
|
||||
options.mutable_sub_options()->add_num_lights(33);
|
||||
node.add_node_options()->PackFrom(options);
|
||||
|
||||
// Retrieve the specified option.
|
||||
FieldData node_data = AsFieldData(node);
|
||||
auto path = field_path;
|
||||
std::string node_extension_type = ExtensionType(std::string(split[1]));
|
||||
FieldData node_options;
|
||||
MP_EXPECT_OK(tool::options_field_util::GetNodeOptions(
|
||||
node_data, node_extension_type, &node_options));
|
||||
FieldData packet_data;
|
||||
MP_EXPECT_OK(tool::options_field_util::GetField(field_path, node_options,
|
||||
&packet_data));
|
||||
EXPECT_EQ(packet_data.value_case(), FieldData::kInt32Value);
|
||||
EXPECT_EQ(packet_data.int32_value(), 33);
|
||||
}
|
||||
|
||||
// TODO: Test with specified extension_type.
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mediapipe
|
||||
|
||||
+39
-62
@@ -207,16 +207,20 @@ cc_library(
|
||||
|
||||
cc_library(
|
||||
name = "gpu_buffer",
|
||||
srcs = ["gpu_buffer.cc"],
|
||||
hdrs = ["gpu_buffer.h"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":gl_base",
|
||||
":gl_context",
|
||||
":gpu_buffer_format",
|
||||
"//mediapipe/framework/formats:image_frame",
|
||||
] + select({
|
||||
"//conditions:default": [
|
||||
":gl_texture_buffer",
|
||||
],
|
||||
"//mediapipe:ios": [
|
||||
"//mediapipe/objc:util",
|
||||
"//mediapipe/objc:CFHolder",
|
||||
],
|
||||
"//mediapipe:macos": [
|
||||
@@ -478,6 +482,7 @@ cc_library(
|
||||
"//mediapipe:ios": [
|
||||
":pixel_buffer_pool_util",
|
||||
"//mediapipe/objc:CFHolder",
|
||||
"//mediapipe/objc:util",
|
||||
],
|
||||
"//mediapipe:macos": [
|
||||
":pixel_buffer_pool_util",
|
||||
@@ -498,55 +503,40 @@ cc_library(
|
||||
],
|
||||
)
|
||||
|
||||
HELPER_ANDROID_SRCS = [
|
||||
"gl_calculator_helper_impl_android.cc",
|
||||
"gl_calculator_helper_impl_common.cc",
|
||||
]
|
||||
|
||||
HELPER_ANDROID_HDRS = [
|
||||
"egl_surface_holder.h",
|
||||
]
|
||||
|
||||
HELPER_COMMON_SRCS = [
|
||||
"gl_calculator_helper.cc",
|
||||
]
|
||||
|
||||
HELPER_COMMON_HDRS = [
|
||||
"gl_calculator_helper.h",
|
||||
"gl_calculator_helper_impl.h",
|
||||
]
|
||||
|
||||
HELPER_IOS_SRCS = [
|
||||
"gl_calculator_helper_impl_ios.mm",
|
||||
"gl_calculator_helper_impl_common.cc",
|
||||
]
|
||||
|
||||
HELPER_IOS_FRAMEWORKS = [
|
||||
"AVFoundation",
|
||||
"CoreVideo",
|
||||
"CoreGraphics",
|
||||
"CoreMedia",
|
||||
"GLKit",
|
||||
"QuartzCore",
|
||||
] + select({
|
||||
"//conditions:default": [
|
||||
"OpenGLES",
|
||||
cc_library(
|
||||
name = "egl_surface_holder",
|
||||
hdrs = ["egl_surface_holder.h"],
|
||||
deps = [
|
||||
":gl_base",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
],
|
||||
"//mediapipe:macos": [
|
||||
"OpenGL",
|
||||
"AppKit",
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "gl_calculator_helper",
|
||||
srcs = select({
|
||||
"//conditions:default": HELPER_COMMON_SRCS + HELPER_ANDROID_SRCS,
|
||||
"//mediapipe:apple": [],
|
||||
}),
|
||||
hdrs = HELPER_COMMON_HDRS + select({
|
||||
"//conditions:default": HELPER_ANDROID_HDRS,
|
||||
"//mediapipe:apple": [],
|
||||
srcs = [
|
||||
"gl_calculator_helper.cc",
|
||||
"gl_calculator_helper_impl_common.cc",
|
||||
],
|
||||
hdrs = [
|
||||
"gl_calculator_helper.h",
|
||||
"gl_calculator_helper_impl.h",
|
||||
],
|
||||
linkopts = select({
|
||||
"//conditions:default": [],
|
||||
"//mediapipe:apple": [
|
||||
"-framework AVFoundation",
|
||||
"-framework CoreVideo",
|
||||
"-framework CoreGraphics",
|
||||
"-framework CoreMedia",
|
||||
"-framework GLKit",
|
||||
"-framework QuartzCore",
|
||||
],
|
||||
}) + select({
|
||||
"//conditions:default": [],
|
||||
"//mediapipe:macos": [
|
||||
"-framework AppKit",
|
||||
],
|
||||
}),
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
@@ -582,34 +572,20 @@ cc_library(
|
||||
] + select({
|
||||
"//conditions:default": [
|
||||
],
|
||||
"//mediapipe:apple": [
|
||||
":gl_calculator_helper_ios",
|
||||
"//mediapipe/objc:util",
|
||||
"//mediapipe/objc:CFHolder",
|
||||
],
|
||||
"//mediapipe:apple": [],
|
||||
}),
|
||||
)
|
||||
|
||||
# TODO: remove
|
||||
objc_library(
|
||||
name = "gl_calculator_helper_ios",
|
||||
srcs = HELPER_COMMON_SRCS + HELPER_IOS_SRCS,
|
||||
hdrs = HELPER_COMMON_HDRS,
|
||||
copts = [
|
||||
"-Wno-shorten-64-to-32",
|
||||
"-std=c++17",
|
||||
],
|
||||
sdk_frameworks = HELPER_IOS_FRAMEWORKS,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":gl_base",
|
||||
":gl_context",
|
||||
":gpu_buffer",
|
||||
":gpu_buffer_multi_pool",
|
||||
":gpu_service",
|
||||
":gpu_shared_data_internal",
|
||||
":shader_util",
|
||||
"//mediapipe/framework:calculator_framework",
|
||||
"//mediapipe/framework/formats:image",
|
||||
":gl_calculator_helper",
|
||||
"//mediapipe/objc:mediapipe_framework_ios",
|
||||
"//mediapipe/objc:util",
|
||||
],
|
||||
@@ -769,6 +745,7 @@ cc_library(
|
||||
srcs = ["gl_surface_sink_calculator.cc"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":egl_surface_holder",
|
||||
":gl_calculator_helper",
|
||||
":gl_quad_renderer",
|
||||
":gpu_buffer",
|
||||
|
||||
@@ -24,15 +24,8 @@
|
||||
#include "mediapipe/gpu/gpu_buffer.h"
|
||||
#include "mediapipe/gpu/gpu_service.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include "mediapipe/objc/util.h"
|
||||
#endif
|
||||
|
||||
namespace mediapipe {
|
||||
|
||||
GlTexture::GlTexture(GLuint name, int width, int height)
|
||||
: name_(name), width_(width), height_(height), target_(GL_TEXTURE_2D) {}
|
||||
|
||||
// The constructor and destructor need to be defined here so that
|
||||
// std::unique_ptr can see the full definition of GlCalculatorHelperImpl.
|
||||
// In the header, it is an incomplete type.
|
||||
|
||||
@@ -31,8 +31,6 @@
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <CoreVideo/CoreVideo.h>
|
||||
|
||||
#include "mediapipe/objc/CFHolder.h"
|
||||
#endif // __APPLE__
|
||||
|
||||
namespace mediapipe {
|
||||
@@ -42,14 +40,6 @@ class GlTexture;
|
||||
class GpuResources;
|
||||
struct GpuSharedData;
|
||||
|
||||
#ifdef __APPLE__
|
||||
#if TARGET_OS_OSX
|
||||
typedef CVOpenGLTextureRef CVTextureType;
|
||||
#else
|
||||
typedef CVOpenGLESTextureRef CVTextureType;
|
||||
#endif // TARGET_OS_OSX
|
||||
#endif // __APPLE__
|
||||
|
||||
using ImageFrameSharedPtr = std::shared_ptr<ImageFrame>;
|
||||
|
||||
// TODO: remove this and Process below, or make Process available
|
||||
@@ -174,14 +164,12 @@ class GlCalculatorHelper {
|
||||
class GlTexture {
|
||||
public:
|
||||
GlTexture() {}
|
||||
GlTexture(GLuint name, int width, int height);
|
||||
|
||||
~GlTexture() { Release(); }
|
||||
|
||||
int width() const { return width_; }
|
||||
int height() const { return height_; }
|
||||
GLenum target() const { return target_; }
|
||||
GLuint name() const { return name_; }
|
||||
int width() const { return view_.width(); }
|
||||
int height() const { return view_.height(); }
|
||||
GLenum target() const { return view_.target(); }
|
||||
GLuint name() const { return view_.name(); }
|
||||
|
||||
// Returns a buffer that can be sent to another calculator.
|
||||
// & manages sync token
|
||||
@@ -190,26 +178,12 @@ class GlTexture {
|
||||
std::unique_ptr<T> GetFrame() const;
|
||||
|
||||
// Releases texture memory & manages sync token
|
||||
void Release();
|
||||
void Release() { view_.Release(); }
|
||||
|
||||
private:
|
||||
explicit GlTexture(GlTextureView view) : view_(std::move(view)) {}
|
||||
friend class GlCalculatorHelperImpl;
|
||||
GlCalculatorHelperImpl* helper_impl_ = nullptr;
|
||||
GLuint name_ = 0;
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
GLenum target_ = GL_TEXTURE_2D;
|
||||
|
||||
#ifdef MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
// For CVPixelBufferRef-based rendering
|
||||
CFHolder<CVTextureType> cv_texture_;
|
||||
#else
|
||||
// Keeps track of whether this texture mapping is for read access, so that
|
||||
// we can create a consumer sync point when releasing it.
|
||||
bool for_reading_ = false;
|
||||
#endif
|
||||
GpuBuffer gpu_buffer_;
|
||||
int plane_ = 0;
|
||||
GlTextureView view_;
|
||||
};
|
||||
|
||||
// Returns the entry with the given tag if the collection uses tags, with the
|
||||
|
||||
@@ -58,19 +58,14 @@ class GlCalculatorHelperImpl {
|
||||
GlContext& GetGlContext() const;
|
||||
|
||||
// For internal use.
|
||||
void ReadTexture(const GlTexture& texture, void* output, size_t size);
|
||||
static void ReadTexture(const GlTextureView& view, void* output, size_t size);
|
||||
|
||||
private:
|
||||
// Makes a GpuBuffer accessible as a texture in the GL context.
|
||||
GlTexture MapGpuBuffer(const GpuBuffer& gpu_buffer, int plane);
|
||||
|
||||
#if !MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
GlTexture MapGlTextureBuffer(const GlTextureBufferSharedPtr& texture_buffer);
|
||||
GlTextureBufferSharedPtr MakeGlTextureBuffer(const ImageFrame& image_frame);
|
||||
#endif // !MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
|
||||
// Sets default texture filtering parameters.
|
||||
void SetStandardTextureParams(GLenum target, GLint internal_format);
|
||||
GlTexture MapGpuBuffer(const GpuBuffer& gpu_buffer, int plane,
|
||||
bool for_reading);
|
||||
void AttachGlTexture(GlTexture& texture, const GpuBuffer& gpu_buffer,
|
||||
int plane, bool for_reading);
|
||||
|
||||
// Create the framebuffer for rendering.
|
||||
void CreateFramebuffer();
|
||||
@@ -80,10 +75,6 @@ class GlCalculatorHelperImpl {
|
||||
GLuint framebuffer_ = 0;
|
||||
|
||||
GpuResources& gpu_resources_;
|
||||
|
||||
// Necessary to compute for a given GlContext in order to properly enforce the
|
||||
// SetStandardTextureParams.
|
||||
bool can_linear_filter_float_textures_;
|
||||
};
|
||||
|
||||
} // namespace mediapipe
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
// 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 <memory>
|
||||
|
||||
#include "mediapipe/gpu/gl_calculator_helper_impl.h"
|
||||
#include "mediapipe/gpu/gpu_shared_data_internal.h"
|
||||
|
||||
namespace mediapipe {
|
||||
|
||||
// TODO: move this method to GlCalculatorHelper, then we can
|
||||
// access its framebuffer instead of requiring that one is already set.
|
||||
template <>
|
||||
std::unique_ptr<ImageFrame> GlTexture::GetFrame<ImageFrame>() const {
|
||||
auto output =
|
||||
absl::make_unique<ImageFrame>(ImageFormat::SRGBA, width_, height_,
|
||||
ImageFrame::kGlDefaultAlignmentBoundary);
|
||||
|
||||
CHECK(helper_impl_);
|
||||
helper_impl_->ReadTexture(*this, output->MutablePixelData(),
|
||||
output->PixelDataSize());
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
template <>
|
||||
std::unique_ptr<GpuBuffer> GlTexture::GetFrame<GpuBuffer>() const {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// When WebGL is used, the GL context may be spontaneously lost which can
|
||||
// cause GpuBuffer allocations to fail. In that case, return a dummy buffer
|
||||
// to allow processing of the current frame complete.
|
||||
if (!gpu_buffer_) {
|
||||
return std::make_unique<GpuBuffer>();
|
||||
}
|
||||
#endif // __EMSCRIPTEN__
|
||||
|
||||
CHECK(gpu_buffer_);
|
||||
// Inform the GlTextureBuffer that we have produced new content, and create
|
||||
// a producer sync point.
|
||||
gpu_buffer_.GetGlTextureBufferSharedPtr()->Updated(
|
||||
helper_impl_->GetGlContext().CreateSyncToken());
|
||||
|
||||
#ifdef __ANDROID__
|
||||
// On (some?) Android devices, the texture may need to be explicitly
|
||||
// detached from the current framebuffer.
|
||||
// TODO: is this necessary even with the unbind in BindFramebuffer?
|
||||
// It is not clear if this affected other contexts too, but let's keep it
|
||||
// while in doubt.
|
||||
GLint type = GL_NONE;
|
||||
glGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE,
|
||||
&type);
|
||||
if (type == GL_TEXTURE) {
|
||||
GLint color_attachment = 0;
|
||||
glGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
|
||||
&color_attachment);
|
||||
if (color_attachment == name_) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Some Android drivers log a GL_INVALID_ENUM error after the first
|
||||
// glGetFramebufferAttachmentParameteriv call if there is no bound object,
|
||||
// even though it should be ok to ask for the type and get back GL_NONE.
|
||||
// Let's just ignore any pending errors here.
|
||||
GLenum error;
|
||||
while ((error = glGetError()) != GL_NO_ERROR) {
|
||||
}
|
||||
|
||||
#endif // __ANDROID__
|
||||
return absl::make_unique<GpuBuffer>(gpu_buffer_);
|
||||
}
|
||||
|
||||
void GlTexture::Release() {
|
||||
if (for_reading_ && gpu_buffer_) {
|
||||
// Inform the GlTextureBuffer that we have finished accessing its contents,
|
||||
// and create a consumer sync point.
|
||||
gpu_buffer_.GetGlTextureBufferSharedPtr()->DidRead(
|
||||
helper_impl_->GetGlContext().CreateSyncToken());
|
||||
}
|
||||
helper_impl_ = nullptr;
|
||||
for_reading_ = false;
|
||||
gpu_buffer_ = nullptr;
|
||||
plane_ = 0;
|
||||
name_ = 0;
|
||||
width_ = 0;
|
||||
height_ = 0;
|
||||
}
|
||||
|
||||
} // namespace mediapipe
|
||||
@@ -25,17 +25,6 @@ GlCalculatorHelperImpl::GlCalculatorHelperImpl(CalculatorContext* cc,
|
||||
GpuResources* gpu_resources)
|
||||
: gpu_resources_(*gpu_resources) {
|
||||
gl_context_ = gpu_resources_.gl_context(cc);
|
||||
// GL_ES_VERSION_2_0 and up (at least through ES 3.2) may contain the extension.
|
||||
// Checking against one also checks against higher ES versions. So this checks
|
||||
// against GLES >= 2.0.
|
||||
#if GL_ES_VERSION_2_0
|
||||
// No linear float filtering by default, check extensions.
|
||||
can_linear_filter_float_textures_ =
|
||||
gl_context_->HasGlExtension("OES_texture_float_linear");
|
||||
#else
|
||||
// Any float32 texture we create should automatically have linear filtering.
|
||||
can_linear_filter_float_textures_ = true;
|
||||
#endif // GL_ES_VERSION_2_0
|
||||
}
|
||||
|
||||
GlCalculatorHelperImpl::~GlCalculatorHelperImpl() {
|
||||
@@ -101,98 +90,59 @@ void GlCalculatorHelperImpl::BindFramebuffer(const GlTexture& dst) {
|
||||
#endif
|
||||
}
|
||||
|
||||
void GlCalculatorHelperImpl::SetStandardTextureParams(GLenum target,
|
||||
GLint internal_format) {
|
||||
// Default to using linear filter everywhere. For float32 textures, fall back
|
||||
// to GL_NEAREST if linear filtering unsupported.
|
||||
GLint filter;
|
||||
switch (internal_format) {
|
||||
case GL_R32F:
|
||||
case GL_RG32F:
|
||||
case GL_RGBA32F:
|
||||
// 32F (unlike 16f) textures do not always support texture filtering
|
||||
// (According to OpenGL ES specification [TEXTURE IMAGE SPECIFICATION])
|
||||
filter = can_linear_filter_float_textures_ ? GL_LINEAR : 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);
|
||||
}
|
||||
GlTexture GlCalculatorHelperImpl::MapGpuBuffer(const GpuBuffer& gpu_buffer,
|
||||
int plane, bool for_reading) {
|
||||
GlTextureView view = gpu_buffer.GetGlTextureView(plane, for_reading);
|
||||
|
||||
#if !MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
GlTexture GlCalculatorHelperImpl::CreateSourceTexture(
|
||||
const ImageFrame& image_frame) {
|
||||
GlTexture texture = MapGlTextureBuffer(MakeGlTextureBuffer(image_frame));
|
||||
texture.for_reading_ = true;
|
||||
return texture;
|
||||
if (gpu_buffer.format() != GpuBufferFormat::kUnknown) {
|
||||
// TODO: do the params need to be reset here??
|
||||
glBindTexture(view.target(), view.name());
|
||||
GlTextureInfo info = GlTextureInfoForGpuBufferFormat(
|
||||
gpu_buffer.format(), view.plane(), GetGlVersion());
|
||||
gl_context_->SetStandardTextureParams(view.target(),
|
||||
info.gl_internal_format);
|
||||
glBindTexture(view.target(), 0);
|
||||
}
|
||||
|
||||
return GlTexture(std::move(view));
|
||||
}
|
||||
|
||||
GlTexture GlCalculatorHelperImpl::CreateSourceTexture(
|
||||
const GpuBuffer& gpu_buffer) {
|
||||
GlTexture texture = MapGpuBuffer(gpu_buffer, 0);
|
||||
texture.for_reading_ = true;
|
||||
return texture;
|
||||
return MapGpuBuffer(gpu_buffer, 0, true);
|
||||
}
|
||||
|
||||
GlTexture GlCalculatorHelperImpl::CreateSourceTexture(
|
||||
const GpuBuffer& gpu_buffer, int plane) {
|
||||
GlTexture texture = MapGpuBuffer(gpu_buffer, plane);
|
||||
texture.for_reading_ = true;
|
||||
return texture;
|
||||
return MapGpuBuffer(gpu_buffer, plane, true);
|
||||
}
|
||||
|
||||
GlTexture GlCalculatorHelperImpl::MapGpuBuffer(const GpuBuffer& gpu_buffer,
|
||||
int plane) {
|
||||
CHECK_EQ(plane, 0);
|
||||
return MapGlTextureBuffer(gpu_buffer.GetGlTextureBufferSharedPtr());
|
||||
}
|
||||
|
||||
GlTexture GlCalculatorHelperImpl::MapGlTextureBuffer(
|
||||
const GlTextureBufferSharedPtr& texture_buffer) {
|
||||
// Insert wait call to sync with the producer.
|
||||
texture_buffer->WaitOnGpu();
|
||||
GlTexture texture;
|
||||
texture.helper_impl_ = this;
|
||||
texture.gpu_buffer_ = GpuBuffer(texture_buffer);
|
||||
texture.plane_ = 0;
|
||||
texture.width_ = texture_buffer->width_;
|
||||
texture.height_ = texture_buffer->height_;
|
||||
texture.target_ = texture_buffer->target_;
|
||||
texture.name_ = texture_buffer->name_;
|
||||
|
||||
if (texture_buffer->format() != GpuBufferFormat::kUnknown) {
|
||||
// TODO: do the params need to be reset here??
|
||||
glBindTexture(texture.target(), texture.name());
|
||||
GlTextureInfo info = GlTextureInfoForGpuBufferFormat(
|
||||
texture_buffer->format(), texture.plane_, GetGlVersion());
|
||||
SetStandardTextureParams(texture.target(), info.gl_internal_format);
|
||||
glBindTexture(texture.target(), 0);
|
||||
}
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
GlTextureBufferSharedPtr GlCalculatorHelperImpl::MakeGlTextureBuffer(
|
||||
GlTexture GlCalculatorHelperImpl::CreateSourceTexture(
|
||||
const ImageFrame& image_frame) {
|
||||
CHECK(gl_context_->IsCurrent());
|
||||
GlTexture texture =
|
||||
MapGpuBuffer(GpuBuffer::CopyingImageFrame(image_frame), 0, true);
|
||||
return texture;
|
||||
}
|
||||
|
||||
auto buffer = GlTextureBuffer::Create(image_frame);
|
||||
template <>
|
||||
std::unique_ptr<ImageFrame> GlTexture::GetFrame<ImageFrame>() const {
|
||||
return view_.gpu_buffer().AsImageFrame();
|
||||
}
|
||||
|
||||
if (buffer->format_ != GpuBufferFormat::kUnknown) {
|
||||
glBindTexture(GL_TEXTURE_2D, buffer->name_);
|
||||
GlTextureInfo info = GlTextureInfoForGpuBufferFormat(
|
||||
buffer->format_, /*plane=*/0, GetGlVersion());
|
||||
SetStandardTextureParams(buffer->target_, info.gl_internal_format);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
template <>
|
||||
std::unique_ptr<GpuBuffer> GlTexture::GetFrame<GpuBuffer>() const {
|
||||
auto gpu_buffer = view_.gpu_buffer();
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// When WebGL is used, the GL context may be spontaneously lost which can
|
||||
// cause GpuBuffer allocations to fail. In that case, return a dummy buffer
|
||||
// to allow processing of the current frame complete.
|
||||
if (!gpu_buffer) {
|
||||
return std::make_unique<GpuBuffer>();
|
||||
}
|
||||
|
||||
return buffer;
|
||||
#endif // __EMSCRIPTEN__
|
||||
view_.DoneWriting();
|
||||
return absl::make_unique<GpuBuffer>(gpu_buffer);
|
||||
}
|
||||
#endif // !MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
|
||||
GlTexture GlCalculatorHelperImpl::CreateDestinationTexture(
|
||||
int width, int height, GpuBufferFormat format) {
|
||||
@@ -202,44 +152,9 @@ GlTexture GlCalculatorHelperImpl::CreateDestinationTexture(
|
||||
|
||||
GpuBuffer buffer =
|
||||
gpu_resources_.gpu_buffer_pool().GetBuffer(width, height, format);
|
||||
GlTexture texture = MapGpuBuffer(buffer, 0);
|
||||
GlTexture texture = MapGpuBuffer(buffer, 0, false);
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
void GlCalculatorHelperImpl::ReadTexture(const GlTexture& texture, void* output,
|
||||
size_t size) {
|
||||
CHECK_GE(size, texture.width_ * texture.height_ * 4);
|
||||
|
||||
GLint current_fbo;
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, ¤t_fbo);
|
||||
CHECK_NE(current_fbo, 0);
|
||||
|
||||
GLint color_attachment_name;
|
||||
glGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
|
||||
&color_attachment_name);
|
||||
if (color_attachment_name != texture.name_) {
|
||||
// Save the viewport. Note that we assume that the color attachment is a
|
||||
// GL_TEXTURE_2D texture.
|
||||
GLint viewport[4];
|
||||
glGetIntegerv(GL_VIEWPORT, viewport);
|
||||
|
||||
// Set the data from GLTexture object.
|
||||
glViewport(0, 0, texture.width_, texture.height_);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
texture.target_, texture.name_, 0);
|
||||
glReadPixels(0, 0, texture.width_, texture.height_, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, output);
|
||||
|
||||
// Restore from the saved viewport and color attachment name.
|
||||
glViewport(viewport[0], viewport[1], viewport[2], viewport[3]);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||
color_attachment_name, 0);
|
||||
} else {
|
||||
glReadPixels(0, 0, texture.width_, texture.height_, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, output);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mediapipe
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
// 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/gl_calculator_helper_impl.h"
|
||||
|
||||
#if TARGET_OS_OSX
|
||||
#import <AppKit/NSOpenGL.h>
|
||||
#else
|
||||
#import <OpenGLES/EAGL.h>
|
||||
#endif // TARGET_OS_OSX
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
#include "absl/memory/memory.h"
|
||||
#include "mediapipe/gpu/gpu_buffer_multi_pool.h"
|
||||
#include "mediapipe/gpu/pixel_buffer_pool_util.h"
|
||||
#include "mediapipe/objc/util.h"
|
||||
|
||||
namespace mediapipe {
|
||||
|
||||
#if MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
GlTexture GlCalculatorHelperImpl::CreateSourceTexture(
|
||||
const mediapipe::ImageFrame& image_frame) {
|
||||
GlTexture texture;
|
||||
|
||||
texture.helper_impl_ = this;
|
||||
texture.width_ = image_frame.Width();
|
||||
texture.height_ = image_frame.Height();
|
||||
auto format = GpuBufferFormatForImageFormat(image_frame.Format());
|
||||
|
||||
GlTextureInfo info = GlTextureInfoForGpuBufferFormat(format, 0, GetGlVersion());
|
||||
|
||||
glGenTextures(1, &texture.name_);
|
||||
glBindTexture(GL_TEXTURE_2D, texture.name_);
|
||||
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, info.gl_internal_format);
|
||||
return texture;
|
||||
}
|
||||
|
||||
GlTexture GlCalculatorHelperImpl::CreateSourceTexture(
|
||||
const GpuBuffer& gpu_buffer) {
|
||||
return MapGpuBuffer(gpu_buffer, 0);
|
||||
}
|
||||
|
||||
GlTexture GlCalculatorHelperImpl::CreateSourceTexture(
|
||||
const GpuBuffer& gpu_buffer, int plane) {
|
||||
return MapGpuBuffer(gpu_buffer, plane);
|
||||
}
|
||||
|
||||
GlTexture GlCalculatorHelperImpl::MapGpuBuffer(
|
||||
const GpuBuffer& gpu_buffer, int plane) {
|
||||
CVReturn err;
|
||||
GlTexture texture;
|
||||
texture.helper_impl_ = this;
|
||||
texture.gpu_buffer_ = gpu_buffer;
|
||||
texture.plane_ = plane;
|
||||
|
||||
const GlTextureInfo info =
|
||||
GlTextureInfoForGpuBufferFormat(gpu_buffer.format(), plane, GetGlVersion());
|
||||
// When scale is not 1, we still give the nominal size of the image.
|
||||
texture.width_ = gpu_buffer.width();
|
||||
texture.height_ = gpu_buffer.height();
|
||||
|
||||
#if TARGET_OS_OSX
|
||||
CVOpenGLTextureRef cv_texture_temp;
|
||||
err = CVOpenGLTextureCacheCreateTextureFromImage(
|
||||
kCFAllocatorDefault, gl_context_->cv_texture_cache(), gpu_buffer.GetCVPixelBufferRef(), NULL,
|
||||
&cv_texture_temp);
|
||||
NSCAssert(cv_texture_temp && !err,
|
||||
@"Error at CVOpenGLTextureCacheCreateTextureFromImage %d", err);
|
||||
texture.cv_texture_.adopt(cv_texture_temp);
|
||||
texture.target_ = CVOpenGLTextureGetTarget(*texture.cv_texture_);
|
||||
texture.name_ = CVOpenGLTextureGetName(*texture.cv_texture_);
|
||||
#else
|
||||
CVOpenGLESTextureRef cv_texture_temp;
|
||||
err = CVOpenGLESTextureCacheCreateTextureFromImage(
|
||||
kCFAllocatorDefault, gl_context_->cv_texture_cache(), gpu_buffer.GetCVPixelBufferRef(), NULL,
|
||||
GL_TEXTURE_2D, info.gl_internal_format, texture.width_ / info.downscale,
|
||||
texture.height_ / info.downscale, info.gl_format, info.gl_type, plane,
|
||||
&cv_texture_temp);
|
||||
NSCAssert(cv_texture_temp && !err,
|
||||
@"Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err);
|
||||
texture.cv_texture_.adopt(cv_texture_temp);
|
||||
texture.target_ = CVOpenGLESTextureGetTarget(*texture.cv_texture_);
|
||||
texture.name_ = CVOpenGLESTextureGetName(*texture.cv_texture_);
|
||||
#endif // TARGET_OS_OSX
|
||||
|
||||
glBindTexture(texture.target(), texture.name());
|
||||
SetStandardTextureParams(texture.target(), info.gl_internal_format);
|
||||
|
||||
return texture;
|
||||
}
|
||||
#endif // MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
|
||||
template<>
|
||||
std::unique_ptr<ImageFrame> GlTexture::GetFrame<ImageFrame>() const {
|
||||
#if MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
if (gpu_buffer_.GetCVPixelBufferRef()) {
|
||||
return CreateImageFrameForCVPixelBuffer(gpu_buffer_.GetCVPixelBufferRef());
|
||||
}
|
||||
|
||||
ImageFormat::Format image_format =
|
||||
ImageFormatForGpuBufferFormat(gpu_buffer_.format());
|
||||
CHECK(helper_impl_);
|
||||
GlTextureInfo info =
|
||||
GlTextureInfoForGpuBufferFormat(gpu_buffer_.format(), plane_, helper_impl_->GetGlVersion());
|
||||
|
||||
auto output = absl::make_unique<ImageFrame>(
|
||||
image_format, width_, height_);
|
||||
|
||||
glReadPixels(0, 0, width_, height_, info.gl_format, info.gl_type,
|
||||
output->MutablePixelData());
|
||||
return output;
|
||||
#else
|
||||
CHECK(gpu_buffer_.format() == GpuBufferFormat::kBGRA32);
|
||||
auto output =
|
||||
absl::make_unique<ImageFrame>(ImageFormat::SRGBA, width_, height_,
|
||||
ImageFrame::kGlDefaultAlignmentBoundary);
|
||||
|
||||
CHECK(helper_impl_);
|
||||
helper_impl_->ReadTexture(*this, output->MutablePixelData(), output->PixelDataSize());
|
||||
|
||||
return output;
|
||||
#endif // MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
}
|
||||
|
||||
template<>
|
||||
std::unique_ptr<GpuBuffer> GlTexture::GetFrame<GpuBuffer>() const {
|
||||
NSCAssert(gpu_buffer_, @"gpu_buffer_ must be valid");
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
CVPixelBufferRef pixel_buffer = gpu_buffer_.GetCVPixelBufferRef();
|
||||
CVReturn err = CVPixelBufferLockBaseAddress(pixel_buffer, 0);
|
||||
NSCAssert(err == kCVReturnSuccess, @"CVPixelBufferLockBaseAddress failed: %d", err);
|
||||
OSType pixel_format = CVPixelBufferGetPixelFormatType(pixel_buffer);
|
||||
size_t bytes_per_row = CVPixelBufferGetBytesPerRow(pixel_buffer);
|
||||
uint8_t* pixel_ptr = static_cast<uint8_t*>(CVPixelBufferGetBaseAddress(pixel_buffer));
|
||||
if (pixel_format == kCVPixelFormatType_32BGRA) {
|
||||
// TODO: restore previous framebuffer? Move this to helper so we can
|
||||
// use BindFramebuffer?
|
||||
glViewport(0, 0, width_, height_);
|
||||
glFramebufferTexture2D(
|
||||
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target_, name_, 0);
|
||||
|
||||
size_t contiguous_bytes_per_row = width_ * 4;
|
||||
if (bytes_per_row == contiguous_bytes_per_row) {
|
||||
glReadPixels(0, 0, width_, height_, GL_BGRA, GL_UNSIGNED_BYTE, pixel_ptr);
|
||||
} else {
|
||||
std::vector<uint8_t> contiguous_buffer(contiguous_bytes_per_row * height_);
|
||||
uint8_t* temp_ptr = contiguous_buffer.data();
|
||||
glReadPixels(0, 0, width_, height_, GL_BGRA, GL_UNSIGNED_BYTE, temp_ptr);
|
||||
for (int i = 0; i < height_; ++i) {
|
||||
memcpy(pixel_ptr, temp_ptr, contiguous_bytes_per_row);
|
||||
temp_ptr += contiguous_bytes_per_row;
|
||||
pixel_ptr += bytes_per_row;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
uint32_t format_big = CFSwapInt32HostToBig(pixel_format);
|
||||
NSLog(@"unsupported pixel format: %.4s", (char*)&format_big);
|
||||
}
|
||||
err = CVPixelBufferUnlockBaseAddress(pixel_buffer, 0);
|
||||
NSCAssert(err == kCVReturnSuccess, @"CVPixelBufferUnlockBaseAddress failed: %d", err);
|
||||
#endif
|
||||
return absl::make_unique<GpuBuffer>(gpu_buffer_);
|
||||
}
|
||||
|
||||
void GlTexture::Release() {
|
||||
#if MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
if (*cv_texture_) {
|
||||
cv_texture_.reset(NULL);
|
||||
} else if (name_) {
|
||||
// This is only needed because of the glGenTextures in
|
||||
// CreateSourceTexture(ImageFrame)... change.
|
||||
glDeleteTextures(1, &name_);
|
||||
}
|
||||
#endif // MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
helper_impl_ = nullptr;
|
||||
gpu_buffer_ = nullptr;
|
||||
plane_ = 0;
|
||||
name_ = 0;
|
||||
width_ = 0;
|
||||
height_ = 0;
|
||||
}
|
||||
|
||||
} // namespace mediapipe
|
||||
@@ -222,6 +222,9 @@ bool GlContext::HasGlExtension(absl::string_view extension) const {
|
||||
// to work with GL_EXTENSIONS for newer GL versions, so we must maintain both
|
||||
// variations of this function.
|
||||
absl::Status GlContext::GetGlExtensions() {
|
||||
// RET_CHECK logs by default, but here we just want to check the precondition;
|
||||
// we'll fall back to the alternative implementation for older versions.
|
||||
RET_CHECK(gl_major_version_ >= 3).SetNoLogging();
|
||||
gl_extensions_.clear();
|
||||
// glGetStringi only introduced in GL 3.0+; so we exit out this function if
|
||||
// we don't have that function defined, regardless of version number reported.
|
||||
@@ -330,13 +333,24 @@ absl::Status GlContext::FinishInitialization(bool create_thread) {
|
||||
|
||||
LOG(INFO) << "GL version: " << gl_major_version_ << "." << gl_minor_version_
|
||||
<< " (" << glGetString(GL_VERSION) << ")";
|
||||
if (gl_major_version_ >= 3) {
|
||||
{
|
||||
auto status = GetGlExtensions();
|
||||
if (status.ok()) {
|
||||
return absl::OkStatus();
|
||||
if (!status.ok()) {
|
||||
status = GetGlExtensionsCompat();
|
||||
}
|
||||
MP_RETURN_IF_ERROR(status);
|
||||
}
|
||||
return GetGlExtensionsCompat();
|
||||
|
||||
#if GL_ES_VERSION_2_0 // This actually means "is GLES available".
|
||||
// No linear float filtering by default, check extensions.
|
||||
can_linear_filter_float_textures_ =
|
||||
HasGlExtension("OES_texture_float_linear");
|
||||
#else
|
||||
// Desktop GL should always allow linear filtering.
|
||||
can_linear_filter_float_textures_ = true;
|
||||
#endif // GL_ES_VERSION_2_0
|
||||
|
||||
return absl::OkStatus();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -841,4 +855,25 @@ const GlTextureInfo& GlTextureInfoForGpuBufferFormat(GpuBufferFormat format,
|
||||
return GlTextureInfoForGpuBufferFormat(format, plane, ctx->GetGlVersion());
|
||||
}
|
||||
|
||||
void GlContext::SetStandardTextureParams(GLenum target, GLint internal_format) {
|
||||
// Default to using linear filter everywhere. For float32 textures, fall back
|
||||
// to GL_NEAREST if linear filtering unsupported.
|
||||
GLint filter;
|
||||
switch (internal_format) {
|
||||
case GL_R32F:
|
||||
case GL_RG32F:
|
||||
case GL_RGBA32F:
|
||||
// 32F (unlike 16f) textures do not always support texture filtering
|
||||
// (According to OpenGL ES specification [TEXTURE IMAGE SPECIFICATION])
|
||||
filter = can_linear_filter_float_textures_ ? GL_LINEAR : 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);
|
||||
}
|
||||
|
||||
} // namespace mediapipe
|
||||
|
||||
@@ -276,6 +276,9 @@ class GlContext : public std::enable_shared_from_this<GlContext> {
|
||||
}).IgnoreError();
|
||||
}
|
||||
|
||||
// Sets default texture filtering parameters.
|
||||
void SetStandardTextureParams(GLenum target, GLint internal_format);
|
||||
|
||||
// These are used for testing specific SyncToken implementations. Do not use
|
||||
// outside of tests.
|
||||
enum class SyncTokenTypeForTest {
|
||||
@@ -342,11 +345,11 @@ class GlContext : public std::enable_shared_from_this<GlContext> {
|
||||
// This wraps a thread_local.
|
||||
static std::weak_ptr<GlContext>& CurrentContext();
|
||||
|
||||
static absl::Status SwitchContext(ContextBinding* old_context,
|
||||
static absl::Status SwitchContext(ContextBinding* saved_context,
|
||||
const ContextBinding& new_context);
|
||||
|
||||
absl::Status EnterContext(ContextBinding* previous_context);
|
||||
absl::Status ExitContext(const ContextBinding* previous_context);
|
||||
absl::Status EnterContext(ContextBinding* saved_context);
|
||||
absl::Status ExitContext(const ContextBinding* saved_context);
|
||||
void DestroyContext();
|
||||
|
||||
bool HasContext() const;
|
||||
@@ -383,7 +386,7 @@ class GlContext : public std::enable_shared_from_this<GlContext> {
|
||||
static void GetCurrentContextBinding(ContextBinding* binding);
|
||||
// Makes the context described by new_context current on this thread.
|
||||
static absl::Status SetCurrentContextBinding(
|
||||
const ContextBinding& new_context);
|
||||
const ContextBinding& new_binding);
|
||||
|
||||
// If not null, a dedicated thread used to execute tasks on this context.
|
||||
// Used on Android due to expensive context switching on some configurations.
|
||||
@@ -396,6 +399,10 @@ class GlContext : public std::enable_shared_from_this<GlContext> {
|
||||
// so we should be fine storing the extension pieces as string_view's.
|
||||
std::set<absl::string_view> gl_extensions_;
|
||||
|
||||
// Used by SetStandardTextureParams. Do we want several of these bools, or a
|
||||
// better mechanism?
|
||||
bool can_linear_filter_float_textures_;
|
||||
|
||||
// Number of glFinish calls completed on the GL thread.
|
||||
// Changes should be guarded by mutex_. However, we use simple atomic
|
||||
// loads for efficiency on the fast path.
|
||||
|
||||
@@ -85,7 +85,7 @@ GlContext::StatusOrGlContext GlContext::Create(EGLContext share_context,
|
||||
return std::move(context);
|
||||
}
|
||||
|
||||
absl::Status GlContext::CreateContextInternal(EGLContext external_context,
|
||||
absl::Status GlContext::CreateContextInternal(EGLContext share_context,
|
||||
int gl_version) {
|
||||
CHECK(gl_version == 2 || gl_version == 3);
|
||||
|
||||
@@ -131,8 +131,7 @@ absl::Status GlContext::CreateContextInternal(EGLContext external_context,
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
context_ =
|
||||
eglCreateContext(display_, config_, external_context, context_attr);
|
||||
context_ = eglCreateContext(display_, config_, share_context, context_attr);
|
||||
int error = eglGetError();
|
||||
RET_CHECK(context_ != EGL_NO_CONTEXT)
|
||||
<< "Could not create GLES " << gl_version << " context; "
|
||||
@@ -149,7 +148,7 @@ absl::Status GlContext::CreateContextInternal(EGLContext external_context,
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status GlContext::CreateContext(EGLContext external_context) {
|
||||
absl::Status GlContext::CreateContext(EGLContext share_context) {
|
||||
EGLint major = 0;
|
||||
EGLint minor = 0;
|
||||
|
||||
@@ -163,11 +162,11 @@ absl::Status GlContext::CreateContext(EGLContext external_context) {
|
||||
LOG(INFO) << "Successfully initialized EGL. Major : " << major
|
||||
<< " Minor: " << minor;
|
||||
|
||||
auto status = CreateContextInternal(external_context, 3);
|
||||
auto status = CreateContextInternal(share_context, 3);
|
||||
if (!status.ok()) {
|
||||
LOG(WARNING) << "Creating a context with OpenGL ES 3 failed: " << status;
|
||||
LOG(WARNING) << "Fall back on OpenGL ES 2.";
|
||||
status = CreateContextInternal(external_context, 2);
|
||||
status = CreateContextInternal(share_context, 2);
|
||||
}
|
||||
MP_RETURN_IF_ERROR(status);
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class GlContext::DedicatedThread {
|
||||
DedicatedThread& operator=(DedicatedThread) = delete;
|
||||
|
||||
absl::Status Run(GlStatusFunction gl_func);
|
||||
void RunWithoutWaiting(GlVoidFunction gl_fund);
|
||||
void RunWithoutWaiting(GlVoidFunction gl_func);
|
||||
|
||||
bool IsCurrentThread();
|
||||
|
||||
|
||||
@@ -175,18 +175,16 @@
|
||||
mediapipe::GlCalculatorHelper helper;
|
||||
helper.InitializeForTest(&gpuData);
|
||||
|
||||
std::vector<std::pair<int, int>> sizes{
|
||||
{200, 300},
|
||||
{200, 299},
|
||||
{196, 300},
|
||||
{194, 300},
|
||||
{193, 300},
|
||||
};
|
||||
for (const auto& width_height : sizes) {
|
||||
mediapipe::GlTexture texture =
|
||||
helper.CreateDestinationTexture(width_height.first, width_height.second);
|
||||
XCTAssertNotEqual(texture.name(), 0);
|
||||
}
|
||||
helper.RunInGlContext([&helper] {
|
||||
std::vector<std::pair<int, int>> sizes{
|
||||
{200, 300}, {200, 299}, {196, 300}, {194, 300}, {193, 300},
|
||||
};
|
||||
for (const auto& width_height : sizes) {
|
||||
mediapipe::GlTexture texture =
|
||||
helper.CreateDestinationTexture(width_height.first, width_height.second);
|
||||
XCTAssertNotEqual(texture.name(), 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testSimpleConversionFromFormat:(OSType)cvPixelFormat {
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
#include "mediapipe/gpu/gpu_buffer.h"
|
||||
|
||||
#include "mediapipe/gpu/gl_context.h"
|
||||
|
||||
#if MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
#include "mediapipe/objc/util.h"
|
||||
#endif // MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
|
||||
namespace mediapipe {
|
||||
|
||||
void GlTextureView::Release() {
|
||||
if (detach_) detach_(*this);
|
||||
detach_ = nullptr;
|
||||
gl_context_ = nullptr;
|
||||
gpu_buffer_ = nullptr;
|
||||
plane_ = 0;
|
||||
name_ = 0;
|
||||
width_ = 0;
|
||||
height_ = 0;
|
||||
}
|
||||
|
||||
#if MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
#if TARGET_OS_OSX
|
||||
typedef CVOpenGLTextureRef CVTextureType;
|
||||
#else
|
||||
typedef CVOpenGLESTextureRef CVTextureType;
|
||||
#endif // TARGET_OS_OSX
|
||||
|
||||
GlTextureView GpuBuffer::GetGlTextureView(int plane, bool for_reading) const {
|
||||
CVReturn err;
|
||||
auto gl_context = GlContext::GetCurrent();
|
||||
CHECK(gl_context);
|
||||
#if TARGET_OS_OSX
|
||||
CVTextureType cv_texture_temp;
|
||||
err = CVOpenGLTextureCacheCreateTextureFromImage(
|
||||
kCFAllocatorDefault, gl_context->cv_texture_cache(),
|
||||
GetCVPixelBufferRef(), NULL, &cv_texture_temp);
|
||||
CHECK(cv_texture_temp && !err)
|
||||
<< "CVOpenGLTextureCacheCreateTextureFromImage failed: " << err;
|
||||
CFHolder<CVTextureType> cv_texture;
|
||||
cv_texture.adopt(cv_texture_temp);
|
||||
return GlTextureView(
|
||||
gl_context.get(), CVOpenGLTextureGetTarget(*cv_texture),
|
||||
CVOpenGLTextureGetName(*cv_texture), width(), height(), *this, plane,
|
||||
[cv_texture](
|
||||
mediapipe::GlTextureView&) { /* only retains cv_texture */ });
|
||||
#else
|
||||
const GlTextureInfo info = GlTextureInfoForGpuBufferFormat(
|
||||
format(), plane, gl_context->GetGlVersion());
|
||||
CVTextureType cv_texture_temp;
|
||||
err = CVOpenGLESTextureCacheCreateTextureFromImage(
|
||||
kCFAllocatorDefault, gl_context->cv_texture_cache(),
|
||||
GetCVPixelBufferRef(), NULL, GL_TEXTURE_2D, info.gl_internal_format,
|
||||
width() / info.downscale, height() / info.downscale, info.gl_format,
|
||||
info.gl_type, plane, &cv_texture_temp);
|
||||
CHECK(cv_texture_temp && !err)
|
||||
<< "CVOpenGLESTextureCacheCreateTextureFromImage failed: " << err;
|
||||
CFHolder<CVTextureType> cv_texture;
|
||||
cv_texture.adopt(cv_texture_temp);
|
||||
return GlTextureView(
|
||||
gl_context.get(), CVOpenGLESTextureGetTarget(*cv_texture),
|
||||
CVOpenGLESTextureGetName(*cv_texture), width(), height(), *this, plane,
|
||||
[cv_texture](
|
||||
mediapipe::GlTextureView&) { /* only retains cv_texture */ });
|
||||
#endif // TARGET_OS_OSX
|
||||
}
|
||||
|
||||
GpuBuffer GpuBuffer::CopyingImageFrame(const ImageFrame& image_frame) {
|
||||
auto maybe_buffer = CreateCVPixelBufferCopyingImageFrame(image_frame);
|
||||
// Converts absl::StatusOr to absl::Status since CHECK_OK() currently only
|
||||
// deals with absl::Status in MediaPipe OSS.
|
||||
CHECK_OK(maybe_buffer.status());
|
||||
return GpuBuffer(std::move(maybe_buffer).value());
|
||||
}
|
||||
|
||||
std::unique_ptr<ImageFrame> GpuBuffer::AsImageFrame() const {
|
||||
CHECK(GetCVPixelBufferRef());
|
||||
return CreateImageFrameForCVPixelBuffer(GetCVPixelBufferRef());
|
||||
}
|
||||
|
||||
void GlTextureView::DoneWriting() const {
|
||||
CHECK(gpu_buffer_);
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
CVPixelBufferRef pixel_buffer = gpu_buffer_.GetCVPixelBufferRef();
|
||||
CVReturn err = CVPixelBufferLockBaseAddress(pixel_buffer, 0);
|
||||
CHECK(err == kCVReturnSuccess)
|
||||
<< "CVPixelBufferLockBaseAddress failed: " << err;
|
||||
OSType pixel_format = CVPixelBufferGetPixelFormatType(pixel_buffer);
|
||||
size_t bytes_per_row = CVPixelBufferGetBytesPerRow(pixel_buffer);
|
||||
uint8_t* pixel_ptr =
|
||||
static_cast<uint8_t*>(CVPixelBufferGetBaseAddress(pixel_buffer));
|
||||
if (pixel_format == kCVPixelFormatType_32BGRA) {
|
||||
// TODO: restore previous framebuffer? Move this to helper so we
|
||||
// can use BindFramebuffer?
|
||||
glViewport(0, 0, width(), height());
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target(),
|
||||
name(), 0);
|
||||
|
||||
size_t contiguous_bytes_per_row = width() * 4;
|
||||
if (bytes_per_row == contiguous_bytes_per_row) {
|
||||
glReadPixels(0, 0, width(), height(), GL_BGRA, GL_UNSIGNED_BYTE,
|
||||
pixel_ptr);
|
||||
} else {
|
||||
std::vector<uint8_t> contiguous_buffer(contiguous_bytes_per_row *
|
||||
height());
|
||||
uint8_t* temp_ptr = contiguous_buffer.data();
|
||||
glReadPixels(0, 0, width(), height(), GL_BGRA, GL_UNSIGNED_BYTE,
|
||||
temp_ptr);
|
||||
for (int i = 0; i < height(); ++i) {
|
||||
memcpy(pixel_ptr, temp_ptr, contiguous_bytes_per_row);
|
||||
temp_ptr += contiguous_bytes_per_row;
|
||||
pixel_ptr += bytes_per_row;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG(ERROR) << "unsupported pixel format: " << pixel_format;
|
||||
}
|
||||
err = CVPixelBufferUnlockBaseAddress(pixel_buffer, 0);
|
||||
CHECK(err == kCVReturnSuccess)
|
||||
<< "CVPixelBufferUnlockBaseAddress failed: " << err;
|
||||
#endif
|
||||
}
|
||||
#endif // MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
|
||||
#if !MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
GlTextureView GpuBuffer::GetGlTextureView(int plane, bool for_reading) const {
|
||||
auto gl_context = GlContext::GetCurrent();
|
||||
CHECK(gl_context);
|
||||
const GlTextureBufferSharedPtr& texture_buffer =
|
||||
GetGlTextureBufferSharedPtr();
|
||||
// Insert wait call to sync with the producer.
|
||||
texture_buffer->WaitOnGpu();
|
||||
CHECK_EQ(plane, 0);
|
||||
GlTextureView::DetachFn detach;
|
||||
if (for_reading) {
|
||||
detach = [](mediapipe::GlTextureView& texture) {
|
||||
// Inform the GlTextureBuffer that we have finished accessing its
|
||||
// contents, and create a consumer sync point.
|
||||
texture.gpu_buffer().GetGlTextureBufferSharedPtr()->DidRead(
|
||||
texture.gl_context()->CreateSyncToken());
|
||||
};
|
||||
}
|
||||
return GlTextureView(gl_context.get(), texture_buffer->target(),
|
||||
texture_buffer->name(), width(), height(), *this, plane,
|
||||
std::move(detach));
|
||||
}
|
||||
|
||||
GpuBuffer GpuBuffer::CopyingImageFrame(const ImageFrame& image_frame) {
|
||||
auto gl_context = GlContext::GetCurrent();
|
||||
CHECK(gl_context);
|
||||
|
||||
auto buffer = GlTextureBuffer::Create(image_frame);
|
||||
|
||||
// TODO: does this need to set the texture params? We set them again when the
|
||||
// texture is actually acccessed via GlTexture[View]. Or should they always be
|
||||
// set on creation?
|
||||
if (buffer->format() != GpuBufferFormat::kUnknown) {
|
||||
glBindTexture(GL_TEXTURE_2D, buffer->name());
|
||||
GlTextureInfo info = GlTextureInfoForGpuBufferFormat(
|
||||
buffer->format(), /*plane=*/0, gl_context->GetGlVersion());
|
||||
gl_context->SetStandardTextureParams(buffer->target(),
|
||||
info.gl_internal_format);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
return GpuBuffer(std::move(buffer));
|
||||
}
|
||||
|
||||
static void ReadTexture(const GlTextureView& view, void* output, size_t size) {
|
||||
// TODO: check buffer size? We could use glReadnPixels where available
|
||||
// (OpenGL ES 3.2, i.e. nowhere). Note that, to fully check that the read
|
||||
// won't overflow the buffer with glReadPixels, we'd also need to check or
|
||||
// reset several glPixelStore parameters (e.g. what if someone had the
|
||||
// ill-advised idea of setting GL_PACK_SKIP_PIXELS?).
|
||||
CHECK(view.gl_context());
|
||||
GlTextureInfo info =
|
||||
GlTextureInfoForGpuBufferFormat(view.gpu_buffer().format(), view.plane(),
|
||||
view.gl_context()->GetGlVersion());
|
||||
|
||||
GLint current_fbo;
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, ¤t_fbo);
|
||||
CHECK_NE(current_fbo, 0);
|
||||
|
||||
GLint color_attachment_name;
|
||||
glGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
|
||||
&color_attachment_name);
|
||||
if (color_attachment_name != view.name()) {
|
||||
// Save the viewport. Note that we assume that the color attachment is a
|
||||
// GL_TEXTURE_2D texture.
|
||||
GLint viewport[4];
|
||||
glGetIntegerv(GL_VIEWPORT, viewport);
|
||||
|
||||
// Set the data from GLTextureView object.
|
||||
glViewport(0, 0, view.width(), view.height());
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, view.target(),
|
||||
view.name(), 0);
|
||||
glReadPixels(0, 0, view.width(), view.height(), info.gl_format,
|
||||
info.gl_type, output);
|
||||
|
||||
// Restore from the saved viewport and color attachment name.
|
||||
glViewport(viewport[0], viewport[1], viewport[2], viewport[3]);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||
color_attachment_name, 0);
|
||||
} else {
|
||||
glReadPixels(0, 0, view.width(), view.height(), info.gl_format,
|
||||
info.gl_type, output);
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<ImageFrame> GpuBuffer::AsImageFrame() const {
|
||||
ImageFormat::Format image_format = ImageFormatForGpuBufferFormat(format());
|
||||
auto output = absl::make_unique<ImageFrame>(
|
||||
image_format, width(), height(), ImageFrame::kGlDefaultAlignmentBoundary);
|
||||
auto view = GetGlTextureView(0, true);
|
||||
ReadTexture(view, output->MutablePixelData(), output->PixelDataSize());
|
||||
return output;
|
||||
}
|
||||
|
||||
void GlTextureView::DoneWriting() const {
|
||||
CHECK(gpu_buffer_);
|
||||
// Inform the GlTextureBuffer that we have produced new content, and create
|
||||
// a producer sync point.
|
||||
gpu_buffer_.GetGlTextureBufferSharedPtr()->Updated(
|
||||
gl_context()->CreateSyncToken());
|
||||
|
||||
#ifdef __ANDROID__
|
||||
// On (some?) Android devices, the texture may need to be explicitly
|
||||
// detached from the current framebuffer.
|
||||
// TODO: is this necessary even with the unbind in BindFramebuffer?
|
||||
// It is not clear if this affected other contexts too, but let's keep it
|
||||
// while in doubt.
|
||||
GLint type = GL_NONE;
|
||||
glGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE,
|
||||
&type);
|
||||
if (type == GL_TEXTURE) {
|
||||
GLint color_attachment = 0;
|
||||
glGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
|
||||
&color_attachment);
|
||||
if (color_attachment == name()) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Some Android drivers log a GL_INVALID_ENUM error after the first
|
||||
// glGetFramebufferAttachmentParameteriv call if there is no bound object,
|
||||
// even though it should be ok to ask for the type and get back GL_NONE.
|
||||
// Let's just ignore any pending errors here.
|
||||
GLenum error;
|
||||
while ((error = glGetError()) != GL_NO_ERROR) {
|
||||
}
|
||||
|
||||
#endif // __ANDROID__
|
||||
}
|
||||
|
||||
#endif // !MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
|
||||
} // namespace mediapipe
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "mediapipe/framework/formats/image_frame.h"
|
||||
#include "mediapipe/gpu/gl_base.h"
|
||||
#include "mediapipe/gpu/gpu_buffer_format.h"
|
||||
|
||||
@@ -32,6 +33,9 @@
|
||||
|
||||
namespace mediapipe {
|
||||
|
||||
class GlContext;
|
||||
class GlTextureView;
|
||||
|
||||
// This class wraps a platform-specific buffer of GPU data.
|
||||
// An instance of GpuBuffer acts as an opaque reference to the underlying
|
||||
// data object.
|
||||
@@ -84,6 +88,19 @@ class GpuBuffer {
|
||||
// Allow assignment from nullptr.
|
||||
GpuBuffer& operator=(std::nullptr_t other);
|
||||
|
||||
// TODO: split into read and write, remove const from write.
|
||||
GlTextureView GetGlTextureView(int plane, bool for_reading) const;
|
||||
|
||||
// Make a GpuBuffer copying the data from an ImageFrame.
|
||||
static GpuBuffer CopyingImageFrame(const ImageFrame& image_frame);
|
||||
|
||||
// Make an ImageFrame, possibly sharing the same data. The data is shared if
|
||||
// the GpuBuffer's storage supports memory sharing; otherwise, it is copied.
|
||||
// In order to work correctly across platforms, callers should always treat
|
||||
// the returned ImageFrame as if it shares memory with the GpuBuffer, i.e.
|
||||
// treat it as immutable if the GpuBuffer must not be modified.
|
||||
std::unique_ptr<ImageFrame> AsImageFrame() const;
|
||||
|
||||
private:
|
||||
#if MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
CFHolder<CVPixelBufferRef> pixel_buffer_;
|
||||
@@ -92,6 +109,51 @@ class GpuBuffer {
|
||||
#endif // MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
};
|
||||
|
||||
class GlTextureView {
|
||||
public:
|
||||
GlTextureView() {}
|
||||
~GlTextureView() { Release(); }
|
||||
// TODO: make this class move-only.
|
||||
|
||||
GlContext* gl_context() const { return gl_context_; }
|
||||
int width() const { return width_; }
|
||||
int height() const { return height_; }
|
||||
GLenum target() const { return target_; }
|
||||
GLuint name() const { return name_; }
|
||||
const GpuBuffer& gpu_buffer() const { return gpu_buffer_; }
|
||||
int plane() const { return plane_; }
|
||||
|
||||
private:
|
||||
friend class GpuBuffer;
|
||||
using DetachFn = std::function<void(GlTextureView&)>;
|
||||
GlTextureView(GlContext* context, GLenum target, GLuint name, int width,
|
||||
int height, GpuBuffer gpu_buffer, int plane, DetachFn detach)
|
||||
: gl_context_(context),
|
||||
target_(target),
|
||||
name_(name),
|
||||
width_(width),
|
||||
height_(height),
|
||||
gpu_buffer_(std::move(gpu_buffer)),
|
||||
plane_(plane),
|
||||
detach_(std::move(detach)) {}
|
||||
|
||||
// TODO: remove this friend declaration.
|
||||
friend class GlTexture;
|
||||
void Release();
|
||||
// TODO: make this non-const.
|
||||
void DoneWriting() const;
|
||||
|
||||
GlContext* gl_context_ = nullptr;
|
||||
GLenum target_ = GL_TEXTURE_2D;
|
||||
GLuint name_ = 0;
|
||||
// Note: when scale is not 1, we still give the nominal size of the image.
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
GpuBuffer gpu_buffer_;
|
||||
int plane_ = 0;
|
||||
DetachFn detach_;
|
||||
};
|
||||
|
||||
#if MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
|
||||
inline int GpuBuffer::width() const {
|
||||
|
||||
@@ -21,10 +21,11 @@
|
||||
#include "mediapipe/framework/port/logging.h"
|
||||
#include "mediapipe/gpu/gpu_shared_data_internal.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#if MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
#include "CoreFoundation/CFBase.h"
|
||||
#include "mediapipe/objc/CFHolder.h"
|
||||
#endif // __APPLE__
|
||||
#include "mediapipe/objc/util.h"
|
||||
#endif // MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
|
||||
|
||||
namespace mediapipe {
|
||||
|
||||
|
||||
@@ -63,11 +63,6 @@ CVReturn CreateCVPixelBufferWithPool(CVPixelBufferPoolRef pool,
|
||||
CFDictionaryRef CreateCVPixelBufferPoolAuxiliaryAttributesForThreshold(
|
||||
int allocationThreshold);
|
||||
|
||||
// Create a CVPixelBuffer without using a pool.
|
||||
CVReturn CreateCVPixelBufferWithoutPool(int width, int height,
|
||||
OSType pixelFormat,
|
||||
CVPixelBufferRef* outBuffer);
|
||||
|
||||
} // namespace mediapipe
|
||||
|
||||
#endif // MEDIAPIPE_GPU_PIXEL_BUFFER_POOL_UTIL_H_
|
||||
|
||||
@@ -121,33 +121,4 @@ CVReturn CreateCVPixelBufferWithPool(
|
||||
return err;
|
||||
}
|
||||
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
static void FreeRefConReleaseCallback(void* refCon, const void* baseAddress) {
|
||||
free(refCon);
|
||||
}
|
||||
#endif
|
||||
|
||||
CVReturn CreateCVPixelBufferWithoutPool(
|
||||
int width, int height, OSType pixelFormat, CVPixelBufferRef* outBuffer) {
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
// On the simulator, syncing the texture with the pixelbuffer does not work,
|
||||
// and we have to use glReadPixels. Since GL_UNPACK_ROW_LENGTH is not
|
||||
// available in OpenGL ES 2, we should create the buffer so the pixels are
|
||||
// contiguous.
|
||||
//
|
||||
// TODO: verify if we can use kIOSurfaceBytesPerRow to force
|
||||
// CoreVideo to give us contiguous data.
|
||||
size_t bytes_per_row = width * 4;
|
||||
void* data = malloc(bytes_per_row * height);
|
||||
return CVPixelBufferCreateWithBytes(
|
||||
kCFAllocatorDefault, width, height, pixelFormat, data, bytes_per_row,
|
||||
FreeRefConReleaseCallback, data, GetCVPixelBufferAttributesForGlCompatibility(),
|
||||
outBuffer);
|
||||
#else
|
||||
return CVPixelBufferCreate(
|
||||
kCFAllocatorDefault, width, height, pixelFormat,
|
||||
GetCVPixelBufferAttributesForGlCompatibility(), outBuffer);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace mediapipe
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace mediapipe {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kNumFaceLandmarkConnections = 124;
|
||||
constexpr int kNumFaceLandmarkConnections = 132;
|
||||
// Pairs of landmark indices to be rendered with connections.
|
||||
constexpr int kFaceLandmarkConnections[] = {
|
||||
// Lips.
|
||||
@@ -43,6 +43,8 @@ constexpr int kFaceLandmarkConnections[] = {
|
||||
133,
|
||||
// Left eyebrow.
|
||||
46, 53, 53, 52, 52, 65, 65, 55, 70, 63, 63, 105, 105, 66, 66, 107,
|
||||
// Left iris.
|
||||
474, 475, 475, 476, 476, 477, 477, 474,
|
||||
// Right eye.
|
||||
263, 249, 249, 390, 390, 373, 373, 374, 374, 380, 380, 381, 381, 382, 382,
|
||||
362, 263, 466, 466, 388, 388, 387, 387, 386, 386, 385, 385, 384, 384, 398,
|
||||
@@ -50,6 +52,8 @@ constexpr int kFaceLandmarkConnections[] = {
|
||||
// Right eyebrow.
|
||||
276, 283, 283, 282, 282, 295, 295, 285, 300, 293, 293, 334, 334, 296, 296,
|
||||
336,
|
||||
// Right iris.
|
||||
469, 470, 470, 471, 471, 472, 472, 469,
|
||||
// Face oval.
|
||||
10, 338, 338, 297, 297, 332, 332, 284, 284, 251, 251, 389, 389, 356, 356,
|
||||
454, 454, 323, 323, 361, 361, 288, 288, 397, 397, 365, 365, 379, 379, 378,
|
||||
|
||||
@@ -22,10 +22,12 @@ node {
|
||||
# Defines side packets for further use in the graph.
|
||||
node {
|
||||
calculator: "ConstantSidePacketCalculator"
|
||||
output_side_packet: "PACKET:num_faces"
|
||||
output_side_packet: "PACKET:0:num_faces"
|
||||
output_side_packet: "PACKET:1:with_attention"
|
||||
node_options: {
|
||||
[type.googleapis.com/mediapipe.ConstantSidePacketCalculatorOptions]: {
|
||||
packet { int_value: 1 }
|
||||
packet { bool_value: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +37,7 @@ node {
|
||||
calculator: "FaceLandmarkFrontCpu"
|
||||
input_stream: "IMAGE:input_video"
|
||||
input_side_packet: "NUM_FACES:num_faces"
|
||||
input_side_packet: "WITH_ATTENTION:with_attention"
|
||||
output_stream: "LANDMARKS:multi_face_landmarks"
|
||||
output_stream: "ROIS_FROM_LANDMARKS:face_rects_from_landmarks"
|
||||
output_stream: "DETECTIONS:face_detections"
|
||||
|
||||
@@ -33,10 +33,12 @@ node {
|
||||
# Defines side packets for further use in the graph.
|
||||
node {
|
||||
calculator: "ConstantSidePacketCalculator"
|
||||
output_side_packet: "PACKET:num_faces"
|
||||
output_side_packet: "PACKET:0:num_faces"
|
||||
output_side_packet: "PACKET:1:with_attention"
|
||||
node_options: {
|
||||
[type.googleapis.com/mediapipe.ConstantSidePacketCalculatorOptions]: {
|
||||
packet { int_value: 1 }
|
||||
packet { bool_value: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +48,7 @@ node {
|
||||
calculator: "FaceLandmarkFrontCpu"
|
||||
input_stream: "IMAGE:throttled_input_video"
|
||||
input_side_packet: "NUM_FACES:num_faces"
|
||||
input_side_packet: "WITH_ATTENTION:with_attention"
|
||||
output_stream: "LANDMARKS:multi_face_landmarks"
|
||||
output_stream: "ROIS_FROM_LANDMARKS:face_rects_from_landmarks"
|
||||
output_stream: "DETECTIONS:face_detections"
|
||||
|
||||
@@ -33,10 +33,12 @@ node {
|
||||
# Defines side packets for further use in the graph.
|
||||
node {
|
||||
calculator: "ConstantSidePacketCalculator"
|
||||
output_side_packet: "PACKET:num_faces"
|
||||
output_side_packet: "PACKET:0:num_faces"
|
||||
output_side_packet: "PACKET:1:with_attention"
|
||||
node_options: {
|
||||
[type.googleapis.com/mediapipe.ConstantSidePacketCalculatorOptions]: {
|
||||
packet { int_value: 1 }
|
||||
packet { bool_value: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +48,7 @@ node {
|
||||
calculator: "FaceLandmarkFrontGpu"
|
||||
input_stream: "IMAGE:throttled_input_video"
|
||||
input_side_packet: "NUM_FACES:num_faces"
|
||||
input_side_packet: "WITH_ATTENTION:with_attention"
|
||||
output_stream: "LANDMARKS:multi_face_landmarks"
|
||||
output_stream: "ROIS_FROM_LANDMARKS:face_rects_from_landmarks"
|
||||
output_stream: "DETECTIONS:face_detections"
|
||||
|
||||
@@ -33,11 +33,23 @@ node {
|
||||
output_stream: "throttled_input_video"
|
||||
}
|
||||
|
||||
# Defines side packets for further use in the graph.
|
||||
node {
|
||||
calculator: "ConstantSidePacketCalculator"
|
||||
output_side_packet: "PACKET:with_attention"
|
||||
node_options: {
|
||||
[type.googleapis.com/mediapipe.ConstantSidePacketCalculatorOptions]: {
|
||||
packet { bool_value: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Subgraph that detects faces and corresponding landmarks.
|
||||
node {
|
||||
calculator: "FaceLandmarkFrontGpu"
|
||||
input_stream: "IMAGE:throttled_input_video"
|
||||
input_side_packet: "NUM_FACES:num_faces"
|
||||
input_side_packet: "WITH_ATTENTION:with_attention"
|
||||
output_stream: "LANDMARKS:multi_face_landmarks"
|
||||
output_stream: "ROIS_FROM_LANDMARKS:face_rects_from_landmarks"
|
||||
output_stream: "DETECTIONS:face_detections"
|
||||
|
||||
@@ -511,7 +511,7 @@ public class ExternalTextureConverter implements TextureFrameProducer {
|
||||
frame.getHeight(),
|
||||
frame.getTimestamp()));
|
||||
}
|
||||
frame.waitUntilReleased();
|
||||
frame.waitUntilReleasedWithGpuSync();
|
||||
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
||||
Log.v(
|
||||
TAG,
|
||||
|
||||
@@ -66,7 +66,9 @@ public class AppTextureFrame implements TextureFrame {
|
||||
|
||||
/**
|
||||
* Waits until the consumer is done with the texture.
|
||||
* @throws InterruptedException
|
||||
*
|
||||
* <p>This does a CPU wait for the texture to be complete.
|
||||
* Use {@link waitUntilReleasedWithGpuSync} whenever possible.
|
||||
*/
|
||||
public void waitUntilReleased() throws InterruptedException {
|
||||
synchronized (this) {
|
||||
@@ -82,6 +84,26 @@ public class AppTextureFrame implements TextureFrame {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until the consumer is done with the texture.
|
||||
*
|
||||
* <p>This method must be called within the application's GL context that will overwrite the
|
||||
* TextureFrame.
|
||||
*/
|
||||
public void waitUntilReleasedWithGpuSync() throws InterruptedException {
|
||||
synchronized (this) {
|
||||
while (inUse && releaseSyncToken == null) {
|
||||
wait();
|
||||
}
|
||||
if (releaseSyncToken != null) {
|
||||
releaseSyncToken.waitOnGpu();
|
||||
releaseSyncToken.release();
|
||||
inUse = false;
|
||||
releaseSyncToken = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the texture is currently in use.
|
||||
*
|
||||
|
||||
@@ -37,9 +37,18 @@ public class GraphTextureFrame implements TextureFrame {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
/** Returns the name of the underlying OpenGL texture. */
|
||||
/**
|
||||
* Returns the name of the underlying OpenGL texture.
|
||||
*
|
||||
* <p>Note: if this texture has been obtained using getTextureFrameDeferredWait, a GPU wait on the
|
||||
* producer sync will be done here. That means this method should be called on the GL context that
|
||||
* will actually use the texture.
|
||||
*/
|
||||
@Override
|
||||
public int getTextureName() {
|
||||
// Note that, if a CPU wait has already been done, the sync point will have been
|
||||
// cleared and this will turn into a no-op. See GlFenceSyncPoint::Wait.
|
||||
nativeGpuWait(nativeBufferHandle);
|
||||
return textureName;
|
||||
}
|
||||
|
||||
@@ -92,4 +101,6 @@ public class GraphTextureFrame implements TextureFrame {
|
||||
private native int nativeGetTextureName(long nativeHandle);
|
||||
private native int nativeGetWidth(long nativeHandle);
|
||||
private native int nativeGetHeight(long nativeHandle);
|
||||
|
||||
private native void nativeGpuWait(long nativeHandle);
|
||||
}
|
||||
|
||||
@@ -288,7 +288,18 @@ public final class PacketGetter {
|
||||
*/
|
||||
public static GraphTextureFrame getTextureFrame(final Packet packet) {
|
||||
return new GraphTextureFrame(
|
||||
nativeGetGpuBuffer(packet.getNativeHandle()), packet.getTimestamp());
|
||||
nativeGetGpuBuffer(packet.getNativeHandle(), /* waitOnCpu= */ true), packet.getTimestamp());
|
||||
}
|
||||
|
||||
/**
|
||||
* Works like {@link #getTextureFrame(Packet)}, but does not insert a CPU wait for the texture's
|
||||
* producer before returning. Instead, a GPU wait will automatically occur when
|
||||
* GraphTextureFrame#getTextureName is called.
|
||||
*/
|
||||
public static GraphTextureFrame getTextureFrameDeferredSync(final Packet packet) {
|
||||
return new GraphTextureFrame(
|
||||
nativeGetGpuBuffer(packet.getNativeHandle(), /* waitOnCpu= */ false),
|
||||
packet.getTimestamp());
|
||||
}
|
||||
|
||||
private static native long nativeGetPacketFromReference(long nativePacketHandle);
|
||||
@@ -356,7 +367,7 @@ public final class PacketGetter {
|
||||
|
||||
private static native int nativeGetGpuBufferName(long nativePacketHandle);
|
||||
|
||||
private static native long nativeGetGpuBuffer(long nativePacketHandle);
|
||||
private static native long nativeGetGpuBuffer(long nativePacketHandle, boolean waitOnCpu);
|
||||
|
||||
private PacketGetter() {}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@ cc_library(
|
||||
"//mediapipe/gpu:gpu_buffer",
|
||||
"//mediapipe/gpu:gpu_shared_data_internal",
|
||||
"//mediapipe/gpu:graph_support",
|
||||
"//mediapipe/gpu:egl_surface_holder",
|
||||
],
|
||||
"//mediapipe/gpu:disable_gpu": [
|
||||
"//mediapipe/gpu:gpu_shared_data_internal",
|
||||
|
||||
@@ -34,6 +34,13 @@ JNIEXPORT jint JNICALL GRAPH_TEXTURE_FRAME_METHOD(nativeGetTextureName)(
|
||||
return (*buffer)->name();
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL GRAPH_TEXTURE_FRAME_METHOD(nativeGpuWait)(
|
||||
JNIEnv* env, jobject thiz, jlong nativeHandle) {
|
||||
GlTextureBufferSharedPtr* buffer =
|
||||
reinterpret_cast<GlTextureBufferSharedPtr*>(nativeHandle);
|
||||
(*buffer)->WaitOnGpu();
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL GRAPH_TEXTURE_FRAME_METHOD(nativeGetWidth)(
|
||||
JNIEnv* env, jobject thiz, jlong nativeHandle) {
|
||||
GlTextureBufferSharedPtr* buffer =
|
||||
|
||||
@@ -31,6 +31,9 @@ JNIEXPORT void JNICALL GRAPH_TEXTURE_FRAME_METHOD(nativeReleaseBuffer)(
|
||||
JNIEXPORT jint JNICALL GRAPH_TEXTURE_FRAME_METHOD(nativeGetTextureName)(
|
||||
JNIEnv* env, jobject thiz, jlong nativeHandle);
|
||||
|
||||
JNIEXPORT void JNICALL GRAPH_TEXTURE_FRAME_METHOD(nativeGpuWait)(
|
||||
JNIEnv* env, jobject thiz, jlong nativeHandle);
|
||||
|
||||
JNIEXPORT jint JNICALL GRAPH_TEXTURE_FRAME_METHOD(nativeGetWidth)(
|
||||
JNIEnv* env, jobject thiz, jlong nativeHandle);
|
||||
|
||||
|
||||
@@ -437,9 +437,8 @@ JNIEXPORT jint JNICALL PACKET_GETTER_METHOD(nativeGetGpuBufferName)(
|
||||
return static_cast<jint>(gpu_buffer.GetGlTextureBufferSharedPtr()->name());
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL PACKET_GETTER_METHOD(nativeGetGpuBuffer)(JNIEnv* env,
|
||||
jobject thiz,
|
||||
jlong packet) {
|
||||
JNIEXPORT jlong JNICALL PACKET_GETTER_METHOD(nativeGetGpuBuffer)(
|
||||
JNIEnv* env, jobject thiz, jlong packet, jboolean wait_on_cpu) {
|
||||
mediapipe::Packet mediapipe_packet =
|
||||
mediapipe::android::Graph::GetPacketFromHandle(packet);
|
||||
mediapipe::GlTextureBufferSharedPtr ptr;
|
||||
@@ -459,7 +458,9 @@ JNIEXPORT jlong JNICALL PACKET_GETTER_METHOD(nativeGetGpuBuffer)(JNIEnv* env,
|
||||
mediapipe_packet.Get<mediapipe::GpuBuffer>();
|
||||
ptr = buffer.GetGlTextureBufferSharedPtr();
|
||||
}
|
||||
ptr->WaitUntilComplete();
|
||||
if (wait_on_cpu) {
|
||||
ptr->WaitUntilComplete();
|
||||
}
|
||||
return reinterpret_cast<intptr_t>(
|
||||
new mediapipe::GlTextureBufferSharedPtr(ptr));
|
||||
}
|
||||
|
||||
@@ -154,9 +154,8 @@ JNIEXPORT jint JNICALL PACKET_GETTER_METHOD(nativeGetGpuBufferName)(
|
||||
|
||||
// Returns a mediapipe::GlTextureBufferSharedPtr*.
|
||||
// This will survive independently of the packet.
|
||||
JNIEXPORT jlong JNICALL PACKET_GETTER_METHOD(nativeGetGpuBuffer)(JNIEnv* env,
|
||||
jobject thiz,
|
||||
jlong packet);
|
||||
JNIEXPORT jlong JNICALL PACKET_GETTER_METHOD(nativeGetGpuBuffer)(
|
||||
JNIEnv* env, jobject thiz, jlong packet, jboolean wait_on_cpu);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
|
||||
@@ -22,7 +22,6 @@ android_library(
|
||||
["*.java"],
|
||||
exclude = [
|
||||
"CameraInput.java",
|
||||
"ResultGlBoundary.java",
|
||||
"ResultGlRenderer.java",
|
||||
"SolutionGlSurfaceView.java",
|
||||
"SolutionGlSurfaceViewRenderer.java",
|
||||
@@ -67,7 +66,6 @@ android_library(
|
||||
android_library(
|
||||
name = "solution_rendering",
|
||||
srcs = [
|
||||
"ResultGlBoundary.java",
|
||||
"ResultGlRenderer.java",
|
||||
"SolutionGlSurfaceView.java",
|
||||
"SolutionGlSurfaceViewRenderer.java",
|
||||
@@ -78,7 +76,6 @@ android_library(
|
||||
"//mediapipe/java/com/google/mediapipe/components:android_components",
|
||||
"//mediapipe/java/com/google/mediapipe/framework:android_framework",
|
||||
"//mediapipe/java/com/google/mediapipe/glutil",
|
||||
"//third_party:autovalue",
|
||||
"@maven//:com_google_guava_guava",
|
||||
],
|
||||
)
|
||||
@@ -91,6 +88,8 @@ cc_binary(
|
||||
# TODO: Add more calculators to support other top-level solutions.
|
||||
deps = [
|
||||
"//mediapipe/java/com/google/mediapipe/framework/jni:mediapipe_framework_jni",
|
||||
"//mediapipe/modules/face_detection:face_detection_full_range_image",
|
||||
"//mediapipe/modules/face_detection:face_detection_short_range_image",
|
||||
"//mediapipe/modules/face_landmark:face_landmark_front_cpu_image",
|
||||
"//mediapipe/modules/face_landmark:face_landmark_front_gpu_image",
|
||||
"//mediapipe/modules/hand_landmark:hand_landmark_tracking_cpu_image",
|
||||
|
||||
@@ -54,7 +54,7 @@ public class ImageSolutionBase extends SolutionBase {
|
||||
eglManager = new EglManager(/*parentContext=*/ null);
|
||||
solutionGraph.setParentGlContext(eglManager.getNativeContext());
|
||||
} catch (MediaPipeException e) {
|
||||
throwException("Error occurs when creating MediaPipe image solution graph. ", e);
|
||||
reportError("Error occurs while creating MediaPipe image solution graph.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,8 +72,8 @@ public class ImageSolutionBase extends SolutionBase {
|
||||
/** Sends a {@link TextureFrame} into solution graph for processing. */
|
||||
public void send(TextureFrame textureFrame) {
|
||||
if (!staticImageMode && textureFrame.getTimestamp() == Long.MIN_VALUE) {
|
||||
throwException(
|
||||
"Error occurs when calling the solution send method. ",
|
||||
reportError(
|
||||
"Error occurs while calling the MediaPipe solution send method.",
|
||||
new MediaPipeException(
|
||||
MediaPipeException.StatusCode.FAILED_PRECONDITION.ordinal(),
|
||||
"TextureFrame's timestamp needs to be explicitly set if not in static image mode."));
|
||||
@@ -98,8 +98,8 @@ public class ImageSolutionBase extends SolutionBase {
|
||||
/** Sends a {@link Bitmap} (static image) into solution graph for processing. */
|
||||
public void send(Bitmap inputBitmap) {
|
||||
if (!staticImageMode) {
|
||||
throwException(
|
||||
"Error occurs when calling the solution send method. ",
|
||||
reportError(
|
||||
"Error occurs while calling the solution send method.",
|
||||
new MediaPipeException(
|
||||
MediaPipeException.StatusCode.FAILED_PRECONDITION.ordinal(),
|
||||
"When not in static image mode, a timestamp associated with the image is required."
|
||||
@@ -112,7 +112,7 @@ public class ImageSolutionBase extends SolutionBase {
|
||||
/** Internal implementation of sending Bitmap/TextureFrame into the MediaPipe solution. */
|
||||
private synchronized <T> void sendImage(T imageObj, long timestamp) {
|
||||
if (lastTimestamp >= timestamp) {
|
||||
throwException(
|
||||
reportError(
|
||||
"The received frame having a smaller timestamp than the processed timestamp.",
|
||||
new MediaPipeException(
|
||||
MediaPipeException.StatusCode.FAILED_PRECONDITION.ordinal(),
|
||||
@@ -123,7 +123,7 @@ public class ImageSolutionBase extends SolutionBase {
|
||||
if (imageObj instanceof TextureFrame) {
|
||||
((TextureFrame) imageObj).release();
|
||||
}
|
||||
throwException(
|
||||
reportError(
|
||||
"The solution graph hasn't been successfully started or error occurs during graph"
|
||||
+ " initializaton.",
|
||||
new MediaPipeException(
|
||||
@@ -140,8 +140,8 @@ public class ImageSolutionBase extends SolutionBase {
|
||||
} else if (imageObj instanceof Bitmap) {
|
||||
imagePacket = packetCreator.createRgbaImage((Bitmap) imageObj);
|
||||
} else {
|
||||
throwException(
|
||||
"The input image type is not supported. ",
|
||||
reportError(
|
||||
"The input image type is not supported.",
|
||||
new MediaPipeException(
|
||||
MediaPipeException.StatusCode.UNIMPLEMENTED.ordinal(),
|
||||
"The input image type is not supported."));
|
||||
@@ -164,7 +164,7 @@ public class ImageSolutionBase extends SolutionBase {
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
if (errorListener != null) {
|
||||
errorListener.onError("Mediapipe error: ", e);
|
||||
errorListener.onError("MediaPipe packet creation error: " + e.getMessage(), e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ public class OutputHandler<T extends SolutionResult> {
|
||||
private ResultListener<T> customResultListener;
|
||||
// The user-defined error listener.
|
||||
private ErrorListener customErrorListener;
|
||||
// Whether the output handler should react to timestamp-bound changes by outputting empty packets.
|
||||
private boolean handleTimestampBoundChanges = false;
|
||||
|
||||
/**
|
||||
* Sets a callback to be invoked to convert a packet list to a solution result object.
|
||||
@@ -61,6 +63,20 @@ public class OutputHandler<T extends SolutionResult> {
|
||||
this.customErrorListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the output handler should react to timestamp-bound changes by outputting empty
|
||||
* packets.
|
||||
*
|
||||
* @param handleTimestampBoundChanges a boolean value.
|
||||
*/
|
||||
public void setHandleTimestampBoundChanges(boolean handleTimestampBoundChanges) {
|
||||
this.handleTimestampBoundChanges = handleTimestampBoundChanges;
|
||||
}
|
||||
|
||||
public boolean handleTimestampBoundChanges() {
|
||||
return handleTimestampBoundChanges;
|
||||
}
|
||||
|
||||
/** Handles a list of output packets. Invoked when packet lists become available. */
|
||||
public void run(List<Packet> packets) {
|
||||
T solutionResult = null;
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright 2021 The MediaPipe Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.mediapipe.solutioncore;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
|
||||
/**
|
||||
* The left, right, bottom, and top boundaries of the visible section on the screen. The boundary
|
||||
* values are typically within the range -1.0 and 1.0.
|
||||
*/
|
||||
@AutoValue
|
||||
public abstract class ResultGlBoundary {
|
||||
|
||||
static ResultGlBoundary create(float left, float right, float bottom, float top) {
|
||||
return new AutoValue_ResultGlBoundary(left, right, bottom, top);
|
||||
}
|
||||
|
||||
public abstract float left();
|
||||
|
||||
public abstract float right();
|
||||
|
||||
public abstract float bottom();
|
||||
|
||||
public abstract float top();
|
||||
}
|
||||
@@ -20,6 +20,16 @@ public interface ResultGlRenderer<T extends ImageSolutionResult> {
|
||||
/** Sets up OpenGL rendering when the surface is created or recreated. */
|
||||
void setupRendering();
|
||||
|
||||
/** Renders the solution result. */
|
||||
void renderResult(T result, ResultGlBoundary boundary);
|
||||
/**
|
||||
* Renders the solution result.
|
||||
*
|
||||
* @param result a solution result object that contains the solution outputs.
|
||||
* @param projectionMatrix a 4 x 4 column-vector matrix stored in column-major order (see also <a
|
||||
* href="https://developer.android.com/reference/android/opengl/Matrix">android.opengl.Matrix</a>).
|
||||
* It is an orthographic projection matrix that maps x and y coordinates in {@code result},
|
||||
* defined in [0, 1]x[0, 1] spanning the entire input image (with a top-left origin), to fit
|
||||
* into the {@link SolutionGlSurfaceView} (with a bottom-left origin) that the input image is
|
||||
* rendered into with potential cropping.
|
||||
*/
|
||||
void renderResult(T result, float[] projectionMatrix);
|
||||
}
|
||||
|
||||
@@ -73,21 +73,24 @@ public class SolutionBase {
|
||||
AndroidAssetUtil.getAssetBytes(context.getAssets(), solutionInfo.binaryGraphPath()));
|
||||
}
|
||||
solutionGraph.addMultiStreamCallback(
|
||||
solutionInfo.outputStreamNames(), outputHandler::run, /*observeTimestampBounds=*/ true);
|
||||
solutionInfo.outputStreamNames(),
|
||||
outputHandler::run,
|
||||
/*observeTimestampBounds=*/ outputHandler.handleTimestampBoundChanges());
|
||||
packetCreator = new AndroidPacketCreator(solutionGraph);
|
||||
} catch (MediaPipeException e) {
|
||||
throwException("Error occurs when creating the MediaPipe solution graph. ", e);
|
||||
reportError("Error occurs while creating the MediaPipe solution graph.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Throws exception with error message. */
|
||||
protected void throwException(String message, MediaPipeException e) {
|
||||
/** Reports error with the detailed error message. */
|
||||
protected void reportError(String message, MediaPipeException e) {
|
||||
String detailedErrorMessage = String.format("%s Error details: %s", message, e.getMessage());
|
||||
if (errorListener != null) {
|
||||
errorListener.onError(message, e);
|
||||
errorListener.onError(detailedErrorMessage, e);
|
||||
} else {
|
||||
Log.e(TAG, message, e);
|
||||
Log.e(TAG, detailedErrorMessage, e);
|
||||
throw e;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,7 +117,7 @@ public class SolutionBase {
|
||||
solutionGraph.startRunningGraph();
|
||||
}
|
||||
} catch (MediaPipeException e) {
|
||||
throwException("Error occurs when starting the MediaPipe solution graph. ", e);
|
||||
reportError("Error occurs while starting the MediaPipe solution graph.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +126,7 @@ public class SolutionBase {
|
||||
try {
|
||||
solutionGraph.waitUntilGraphIdle();
|
||||
} catch (MediaPipeException e) {
|
||||
throwException("Error occurs when waiting until the MediaPipe graph becomes idle. ", e);
|
||||
reportError("Error occurs while waiting until the MediaPipe graph becomes idle.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,12 +140,12 @@ public class SolutionBase {
|
||||
// Note: errors during Process are reported at the earliest opportunity,
|
||||
// which may be addPacket or waitUntilDone, depending on timing. For consistency,
|
||||
// we want to always report them using the same async handler if installed.
|
||||
throwException("Error occurs when closing the Mediapipe solution graph. ", e);
|
||||
reportError("Error occurs while closing the Mediapipe solution graph.", e);
|
||||
}
|
||||
try {
|
||||
solutionGraph.tearDown();
|
||||
} catch (MediaPipeException e) {
|
||||
throwException("Error occurs when closing the Mediapipe solution graph. ", e);
|
||||
reportError("Error occurs while closing the Mediapipe solution graph.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-8
@@ -16,6 +16,7 @@ package com.google.mediapipe.solutioncore;
|
||||
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.opengl.GLES20;
|
||||
import android.opengl.Matrix;
|
||||
import com.google.mediapipe.components.GlSurfaceViewRenderer;
|
||||
import com.google.mediapipe.framework.TextureFrame;
|
||||
import com.google.mediapipe.glutil.ShaderUtil;
|
||||
@@ -91,14 +92,18 @@ public class SolutionGlSurfaceViewRenderer<T extends ImageSolutionResult>
|
||||
if (nextSolutionResult != null) {
|
||||
solutionResult = nextSolutionResult.getAndSet(null);
|
||||
float[] textureBoundary = calculateTextureBoundary();
|
||||
// Scales the values from [0, 1] to [-1, 1].
|
||||
ResultGlBoundary resultGlBoundary =
|
||||
ResultGlBoundary.create(
|
||||
textureBoundary[0] * 2 - 1,
|
||||
textureBoundary[1] * 2 - 1,
|
||||
textureBoundary[2] * 2 - 1,
|
||||
textureBoundary[3] * 2 - 1);
|
||||
resultGlRenderer.renderResult(solutionResult, resultGlBoundary);
|
||||
float[] projectionMatrix = new float[16];
|
||||
// See {@link ResultGlRenderer#renderResult}.
|
||||
Matrix.orthoM(
|
||||
projectionMatrix, /* result */
|
||||
0, /* offset */
|
||||
textureBoundary[0], /* left */
|
||||
textureBoundary[1], /* right */
|
||||
textureBoundary[3], /* bottom */
|
||||
textureBoundary[2], /* top */
|
||||
-1, /* near */
|
||||
1 /* far */);
|
||||
resultGlRenderer.renderResult(solutionResult, projectionMatrix);
|
||||
}
|
||||
flush(frame);
|
||||
if (solutionResult != null) {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.mediapipe.solutions.facedetection">
|
||||
|
||||
<uses-sdk android:minSdkVersion="21"
|
||||
android:targetSdkVersion="27" />
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,45 @@
|
||||
# Copyright 2021 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
android_library(
|
||||
name = "facedetection",
|
||||
srcs = [
|
||||
"FaceDetection.java",
|
||||
"FaceDetectionOptions.java",
|
||||
"FaceDetectionResult.java",
|
||||
"FaceKeypoint.java",
|
||||
],
|
||||
assets = [
|
||||
"//mediapipe/modules/face_detection:face_detection_full_range_image.binarypb",
|
||||
"//mediapipe/modules/face_detection:face_detection_full_range_sparse.tflite",
|
||||
"//mediapipe/modules/face_detection:face_detection_short_range.tflite",
|
||||
"//mediapipe/modules/face_detection:face_detection_short_range_image.binarypb",
|
||||
],
|
||||
assets_dir = "",
|
||||
javacopts = ["-Acom.google.auto.value.AutoBuilderIsUnstable"],
|
||||
manifest = ":AndroidManifest.xml",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//mediapipe/framework/formats:detection_java_proto_lite",
|
||||
"//mediapipe/framework/formats:location_data_java_proto_lite",
|
||||
"//mediapipe/java/com/google/mediapipe/framework:android_framework",
|
||||
"//mediapipe/java/com/google/mediapipe/solutioncore:solution_base",
|
||||
"//third_party:autovalue",
|
||||
"@maven//:androidx_annotation_annotation",
|
||||
"@maven//:com_google_code_findbugs_jsr305",
|
||||
"@maven//:com_google_guava_guava",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright 2021 The MediaPipe Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.mediapipe.solutions.facedetection;
|
||||
|
||||
import android.content.Context;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.mediapipe.formats.proto.DetectionProto.Detection;
|
||||
import com.google.mediapipe.formats.proto.LocationDataProto.LocationData.RelativeKeypoint;
|
||||
import com.google.mediapipe.framework.MediaPipeException;
|
||||
import com.google.mediapipe.framework.Packet;
|
||||
import com.google.mediapipe.solutioncore.ErrorListener;
|
||||
import com.google.mediapipe.solutioncore.ImageSolutionBase;
|
||||
import com.google.mediapipe.solutioncore.OutputHandler;
|
||||
import com.google.mediapipe.solutioncore.ResultListener;
|
||||
import com.google.mediapipe.solutioncore.SolutionInfo;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* MediaPipe Face Detection Solution API.
|
||||
*
|
||||
* <p>MediaPipe Face Detection processes a {@link TextureFrame} or a {@link Bitmap} and returns the
|
||||
* {@link FaceDetectionResult} representing each detected face. Please refer to
|
||||
* https://solutions.mediapipe.dev/face_detection#android-solution-api for usage examples.
|
||||
*/
|
||||
public class FaceDetection extends ImageSolutionBase {
|
||||
private static final String TAG = "FaceDetection";
|
||||
|
||||
private static final String SHORT_RANGE_GRAPH_NAME = "face_detection_short_range_image.binarypb";
|
||||
private static final String FULL_RANGE_GRAPH_NAME = "face_detection_full_range_image.binarypb";
|
||||
private static final String IMAGE_INPUT_STREAM = "image";
|
||||
private static final ImmutableList<String> OUTPUT_STREAMS =
|
||||
ImmutableList.of("detections", "throttled_image");
|
||||
private static final int DETECTIONS_INDEX = 0;
|
||||
private static final int INPUT_IMAGE_INDEX = 1;
|
||||
private final OutputHandler<FaceDetectionResult> outputHandler;
|
||||
|
||||
/**
|
||||
* Initializes MediaPipe Face Detection solution.
|
||||
*
|
||||
* @param context an Android {@link Context}.
|
||||
* @param options the configuration options defined in {@link FaceDetectionOptions}.
|
||||
*/
|
||||
public FaceDetection(Context context, FaceDetectionOptions options) {
|
||||
outputHandler = new OutputHandler<>();
|
||||
outputHandler.setOutputConverter(
|
||||
packets -> {
|
||||
FaceDetectionResult.Builder faceMeshResultBuilder = FaceDetectionResult.builder();
|
||||
try {
|
||||
faceMeshResultBuilder.setMultiFaceDetections(
|
||||
getProtoVector(packets.get(DETECTIONS_INDEX), Detection.parser()));
|
||||
} catch (MediaPipeException e) {
|
||||
reportError("Error occurs while getting MediaPipe face detection results.", e);
|
||||
}
|
||||
return faceMeshResultBuilder
|
||||
.setImagePacket(packets.get(INPUT_IMAGE_INDEX))
|
||||
.setTimestamp(
|
||||
staticImageMode ? Long.MIN_VALUE : packets.get(INPUT_IMAGE_INDEX).getTimestamp())
|
||||
.build();
|
||||
});
|
||||
|
||||
SolutionInfo solutionInfo =
|
||||
SolutionInfo.builder()
|
||||
.setBinaryGraphPath(
|
||||
options.modelSelection() == 0 ? SHORT_RANGE_GRAPH_NAME : FULL_RANGE_GRAPH_NAME)
|
||||
.setImageInputStreamName(IMAGE_INPUT_STREAM)
|
||||
.setOutputStreamNames(OUTPUT_STREAMS)
|
||||
.setStaticImageMode(options.staticImageMode())
|
||||
.build();
|
||||
|
||||
initialize(context, solutionInfo, outputHandler);
|
||||
Map<String, Packet> emptyInputSidePackets = new HashMap<>();
|
||||
start(emptyInputSidePackets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a callback to be invoked when a {@link FaceDetectionResult} becomes available.
|
||||
*
|
||||
* @param listener the {@link ResultListener} callback.
|
||||
*/
|
||||
public void setResultListener(ResultListener<FaceDetectionResult> listener) {
|
||||
this.outputHandler.setResultListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a callback to be invoked when the Face Detection solution throws errors.
|
||||
*
|
||||
* @param listener the {@link ErrorListener} callback.
|
||||
*/
|
||||
public void setErrorListener(@Nullable ErrorListener listener) {
|
||||
this.outputHandler.setErrorListener(listener);
|
||||
this.errorListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific face keypoint by face index and face keypoint type.
|
||||
*
|
||||
* @param result the returned {@link FaceDetectionResult} object.
|
||||
* @param faceIndex the face index. A smaller index maps to a detected face with a higher
|
||||
* confidence score.
|
||||
* @param faceKeypointType the face keypoint type defined in {@link FaceKeypoint}.
|
||||
*/
|
||||
public static RelativeKeypoint getFaceKeypoint(
|
||||
FaceDetectionResult result,
|
||||
int faceIndex,
|
||||
@FaceKeypoint.FaceKeypointType int faceKeypointType) {
|
||||
if (result == null
|
||||
|| faceIndex >= result.multiFaceDetections().size()
|
||||
|| faceKeypointType >= FaceKeypoint.NUM_KEY_POINTS) {
|
||||
return RelativeKeypoint.getDefaultInstance();
|
||||
}
|
||||
Detection detection = result.multiFaceDetections().get(faceIndex);
|
||||
float x = detection.getLocationData().getRelativeKeypoints(faceKeypointType).getX();
|
||||
float y = detection.getLocationData().getRelativeKeypoints(faceKeypointType).getY();
|
||||
return RelativeKeypoint.newBuilder().setX(x).setY(y).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2021 The MediaPipe Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.mediapipe.solutions.facedetection;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
|
||||
/**
|
||||
* MediaPipe Face Detection solution-specific options.
|
||||
*
|
||||
* <p>staticImageMode: Whether to treat the input images as a batch of static and possibly unrelated
|
||||
* images, or a video stream. Default to false. See details in
|
||||
* https://solutions.mediapipe.dev/face_detection#static_image_mode.
|
||||
*
|
||||
* <p>minDetectionConfidence: Minimum confidence value ([0.0, 1.0]) for face detection to be
|
||||
* considered successful. See details in
|
||||
* https://solutions.mediapipe.dev/face_detection#min_detection_confidence.
|
||||
*
|
||||
* <p>modelSelection: 0 or 1. 0 to select a short-range model that works best for faces within 2
|
||||
* meters from the camera, and 1 for a full-range model best for faces within 5 meters. See details
|
||||
* in https://solutions.mediapipe.dev/face_detection#model_selection.
|
||||
*/
|
||||
@AutoValue
|
||||
public abstract class FaceDetectionOptions {
|
||||
public abstract boolean staticImageMode();
|
||||
|
||||
public abstract int modelSelection();
|
||||
|
||||
public abstract float minDetectionConfidence();
|
||||
|
||||
public static Builder builder() {
|
||||
return new AutoValue_FaceDetectionOptions.Builder().withDefaultValues();
|
||||
}
|
||||
|
||||
/** Builder for {@link FaceDetectionOptions}. */
|
||||
@AutoValue.Builder
|
||||
public abstract static class Builder {
|
||||
public Builder withDefaultValues() {
|
||||
return setStaticImageMode(false).setModelSelection(0).setMinDetectionConfidence(0.5f);
|
||||
}
|
||||
|
||||
public abstract Builder setStaticImageMode(boolean value);
|
||||
|
||||
public abstract Builder setModelSelection(int value);
|
||||
|
||||
public abstract Builder setMinDetectionConfidence(float value);
|
||||
|
||||
public abstract FaceDetectionOptions build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2021 The MediaPipe Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.mediapipe.solutions.facedetection;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import com.google.auto.value.AutoBuilder;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.mediapipe.formats.proto.DetectionProto.Detection;
|
||||
import com.google.mediapipe.framework.Packet;
|
||||
import com.google.mediapipe.framework.TextureFrame;
|
||||
import com.google.mediapipe.solutioncore.ImageSolutionResult;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* FaceDetectionResult contains the detected faces, and the input {@link Bitmap} or {@link
|
||||
* TextureFrame}. If not in static image mode, the timestamp field will be set to the timestamp of
|
||||
* the corresponding input image.
|
||||
*/
|
||||
public class FaceDetectionResult extends ImageSolutionResult {
|
||||
private final ImmutableList<Detection> multiFaceDetections;
|
||||
|
||||
FaceDetectionResult(
|
||||
ImmutableList<Detection> multiFaceDetections, Packet imagePacket, long timestamp) {
|
||||
this.multiFaceDetections = multiFaceDetections;
|
||||
this.timestamp = timestamp;
|
||||
this.imagePacket = imagePacket;
|
||||
}
|
||||
|
||||
// Collection of detected faces, where each face is represented as a detection proto message that
|
||||
// contains a bounding box and 6 {@link FaceKeypoint}s. The bounding box is composed of xmin and
|
||||
// width (both normalized to [0.0, 1.0] by the image width) and ymin and height (both normalized
|
||||
// to [0.0, 1.0] by the image height). Each keypoint is composed of x and y, which are normalized
|
||||
// to [0.0, 1.0] by the image width and height respectively.
|
||||
public ImmutableList<Detection> multiFaceDetections() {
|
||||
return multiFaceDetections;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new AutoBuilder_FaceDetectionResult_Builder();
|
||||
}
|
||||
|
||||
/** Builder for {@link FaceDetectionResult}. */
|
||||
@AutoBuilder
|
||||
public abstract static class Builder {
|
||||
abstract Builder setMultiFaceDetections(List<Detection> value);
|
||||
|
||||
abstract Builder setTimestamp(long value);
|
||||
|
||||
abstract Builder setImagePacket(Packet value);
|
||||
|
||||
abstract FaceDetectionResult build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2021 The MediaPipe Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.mediapipe.solutions.facedetection;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
|
||||
/** The 6 face keypoints. */
|
||||
public final class FaceKeypoint {
|
||||
public static final int NUM_KEY_POINTS = 6;
|
||||
|
||||
public static final int RIGHT_EYE = 0;
|
||||
public static final int LEFT_EYE = 1;
|
||||
public static final int NOSE_TIP = 2;
|
||||
public static final int MOUTH_CENTER = 3;
|
||||
public static final int RIGHT_EAR_TRAGION = 4;
|
||||
public static final int LEFT_EAR_TRAGION = 5;
|
||||
|
||||
/** Represents a face keypoint type. */
|
||||
@IntDef({
|
||||
RIGHT_EYE,
|
||||
LEFT_EYE,
|
||||
NOSE_TIP,
|
||||
MOUTH_CENTER,
|
||||
RIGHT_EAR_TRAGION,
|
||||
LEFT_EAR_TRAGION,
|
||||
})
|
||||
public @interface FaceKeypointType {}
|
||||
|
||||
private FaceKeypoint() {}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ android_library(
|
||||
assets = [
|
||||
"//mediapipe/modules/face_detection:face_detection_short_range.tflite",
|
||||
"//mediapipe/modules/face_landmark:face_landmark.tflite",
|
||||
"//mediapipe/modules/face_landmark:face_landmark_with_attention.tflite",
|
||||
"//mediapipe/modules/face_landmark:face_landmark_front_cpu_image.binarypb",
|
||||
"//mediapipe/modules/face_landmark:face_landmark_front_gpu_image.binarypb",
|
||||
],
|
||||
|
||||
@@ -29,41 +29,46 @@ import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* MediaPipe FaceMesh Solution API.
|
||||
* MediaPipe Face Mesh Solution API.
|
||||
*
|
||||
* <p>MediaPipe FaceMesh processes a {@link TextureFrame} or a {@link Bitmap} and returns the face
|
||||
* <p>MediaPipe Face Mesh processes a {@link TextureFrame} or a {@link Bitmap} and returns the face
|
||||
* landmarks of each detected face. Please refer to
|
||||
* https://solutions.mediapipe.dev/face_mesh#android-solution-api for usage examples.
|
||||
*/
|
||||
public class FaceMesh extends ImageSolutionBase {
|
||||
private static final String TAG = "FaceMesh";
|
||||
|
||||
public static final int FACEMESH_NUM_LANDMARKS = 468;
|
||||
public static final int FACEMESH_NUM_LANDMARKS_WITH_IRISES = 478;
|
||||
|
||||
private static final String NUM_FACES = "num_faces";
|
||||
private static final String WITH_ATTENTION = "with_attention";
|
||||
private static final String USE_PREV_LANDMARKS = "use_prev_landmarks";
|
||||
private static final String GPU_GRAPH_NAME = "face_landmark_front_gpu_image.binarypb";
|
||||
private static final String CPU_GRAPH_NAME = "face_landmark_front_cpu_image.binarypb";
|
||||
private static final String IMAGE_INPUT_STREAM = "image";
|
||||
private static final ImmutableList<String> OUTPUT_STREAMS =
|
||||
ImmutableList.of("multi_face_landmarks", "image");
|
||||
ImmutableList.of("multi_face_landmarks", "throttled_image");
|
||||
private static final int LANDMARKS_INDEX = 0;
|
||||
private static final int INPUT_IMAGE_INDEX = 1;
|
||||
private final OutputHandler<FaceMeshResult> graphOutputHandler;
|
||||
private final OutputHandler<FaceMeshResult> outputHandler;
|
||||
|
||||
/**
|
||||
* Initializes MediaPipe FaceMesh solution.
|
||||
* Initializes MediaPipe Face Mesh solution.
|
||||
*
|
||||
* @param context an Android {@link Context}.
|
||||
* @param options the configuration options defined in {@link FaceMeshOptions}.
|
||||
*/
|
||||
public FaceMesh(Context context, FaceMeshOptions options) {
|
||||
graphOutputHandler = new OutputHandler<>();
|
||||
graphOutputHandler.setOutputConverter(
|
||||
outputHandler = new OutputHandler<>();
|
||||
outputHandler.setOutputConverter(
|
||||
packets -> {
|
||||
FaceMeshResult.Builder faceMeshResultBuilder = FaceMeshResult.builder();
|
||||
try {
|
||||
faceMeshResultBuilder.setMultiFaceLandmarks(
|
||||
getProtoVector(packets.get(LANDMARKS_INDEX), NormalizedLandmarkList.parser()));
|
||||
} catch (MediaPipeException e) {
|
||||
throwException("Error occurs when getting MediaPipe facemesh landmarks. ", e);
|
||||
reportError("Error occurs when getting MediaPipe facemesh landmarks.", e);
|
||||
}
|
||||
return faceMeshResultBuilder
|
||||
.setImagePacket(packets.get(INPUT_IMAGE_INDEX))
|
||||
@@ -77,31 +82,33 @@ public class FaceMesh extends ImageSolutionBase {
|
||||
.setBinaryGraphPath(options.runOnGpu() ? GPU_GRAPH_NAME : CPU_GRAPH_NAME)
|
||||
.setImageInputStreamName(IMAGE_INPUT_STREAM)
|
||||
.setOutputStreamNames(OUTPUT_STREAMS)
|
||||
.setStaticImageMode(options.mode() == FaceMeshOptions.STATIC_IMAGE_MODE)
|
||||
.setStaticImageMode(options.staticImageMode())
|
||||
.build();
|
||||
|
||||
initialize(context, solutionInfo, graphOutputHandler);
|
||||
initialize(context, solutionInfo, outputHandler);
|
||||
Map<String, Packet> inputSidePackets = new HashMap<>();
|
||||
inputSidePackets.put(NUM_FACES, packetCreator.createInt32(options.maxNumFaces()));
|
||||
inputSidePackets.put(WITH_ATTENTION, packetCreator.createBool(options.refineLandmarks()));
|
||||
inputSidePackets.put(USE_PREV_LANDMARKS, packetCreator.createBool(!options.staticImageMode()));
|
||||
start(inputSidePackets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a callback to be invoked when the FaceMeshResults become available.
|
||||
* Sets a callback to be invoked when a {@link FaceMeshResult} becomes available.
|
||||
*
|
||||
* @param listener the {@link ResultListener} callback.
|
||||
*/
|
||||
public void setResultListener(ResultListener<FaceMeshResult> listener) {
|
||||
this.graphOutputHandler.setResultListener(listener);
|
||||
this.outputHandler.setResultListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a callback to be invoked when the FaceMesh solution throws errors.
|
||||
* Sets a callback to be invoked when the Face Mesh solution throws errors.
|
||||
*
|
||||
* @param listener the {@link ErrorListener} callback.
|
||||
*/
|
||||
public void setErrorListener(@Nullable ErrorListener listener) {
|
||||
this.graphOutputHandler.setErrorListener(listener);
|
||||
this.outputHandler.setErrorListener(listener);
|
||||
this.errorListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ public final class FaceMeshConnections {
|
||||
Connection.create(384, 398),
|
||||
Connection.create(398, 362));
|
||||
|
||||
public static final ImmutableSet<Connection> FACEMESH_LEFT_EYEBR0W =
|
||||
public static final ImmutableSet<Connection> FACEMESH_LEFT_EYEBROW =
|
||||
ImmutableSet.of(
|
||||
Connection.create(276, 283),
|
||||
Connection.create(283, 282),
|
||||
@@ -105,6 +105,13 @@ public final class FaceMeshConnections {
|
||||
Connection.create(334, 296),
|
||||
Connection.create(296, 336));
|
||||
|
||||
public static final ImmutableSet<Connection> FACEMESH_LEFT_IRIS =
|
||||
ImmutableSet.of(
|
||||
Connection.create(474, 475),
|
||||
Connection.create(475, 476),
|
||||
Connection.create(476, 477),
|
||||
Connection.create(477, 474));
|
||||
|
||||
public static final ImmutableSet<Connection> FACEMESH_RIGHT_EYE =
|
||||
ImmutableSet.of(
|
||||
Connection.create(33, 7),
|
||||
@@ -123,6 +130,7 @@ public final class FaceMeshConnections {
|
||||
Connection.create(158, 157),
|
||||
Connection.create(157, 173),
|
||||
Connection.create(173, 133));
|
||||
|
||||
public static final ImmutableSet<Connection> FACEMESH_RIGHT_EYEBROW =
|
||||
ImmutableSet.of(
|
||||
Connection.create(46, 53),
|
||||
@@ -134,6 +142,13 @@ public final class FaceMeshConnections {
|
||||
Connection.create(105, 66),
|
||||
Connection.create(66, 107));
|
||||
|
||||
public static final ImmutableSet<Connection> FACEMESH_RIGHT_IRIS =
|
||||
ImmutableSet.of(
|
||||
Connection.create(469, 470),
|
||||
Connection.create(470, 471),
|
||||
Connection.create(471, 472),
|
||||
Connection.create(472, 469));
|
||||
|
||||
public static final ImmutableSet<Connection> FACEMESH_FACE_OVAL =
|
||||
ImmutableSet.of(
|
||||
Connection.create(10, 338),
|
||||
@@ -177,7 +192,7 @@ public final class FaceMeshConnections {
|
||||
ImmutableSet.<Connection>builder()
|
||||
.addAll(FACEMESH_LIPS)
|
||||
.addAll(FACEMESH_LEFT_EYE)
|
||||
.addAll(FACEMESH_LEFT_EYEBR0W)
|
||||
.addAll(FACEMESH_LEFT_EYEBROW)
|
||||
.addAll(FACEMESH_RIGHT_EYE)
|
||||
.addAll(FACEMESH_RIGHT_EYEBROW)
|
||||
.addAll(FACEMESH_FACE_OVAL)
|
||||
|
||||
@@ -14,18 +14,22 @@
|
||||
|
||||
package com.google.mediapipe.solutions.facemesh;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import com.google.auto.value.AutoValue;
|
||||
|
||||
/**
|
||||
* MediaPipe FaceMesh solution-specific options.
|
||||
*
|
||||
* <p>mode: Whether to treat the input images as a batch of static and possibly unrelated images, or
|
||||
* a video stream. See details in https://solutions.mediapipe.dev/face_mesh#static_image_mode.
|
||||
* <p>staticImageMode: Whether to treat the input images as a batch of static and possibly unrelated
|
||||
* images, or a video stream. Default to false. See details in
|
||||
* https://solutions.mediapipe.dev/face_mesh#static_image_mode.
|
||||
*
|
||||
* <p>maxNumFaces: Maximum number of faces to detect. See details in
|
||||
* https://solutions.mediapipe.dev/face_mesh#max_num_faces.
|
||||
*
|
||||
* <p>refineLandmarks: Whether to further refine the landmark coordinates around the eyes, lips and
|
||||
* face oval, and output additional landmarks around the irises. Default to False. See details in
|
||||
* https://solutions.mediapipe.dev/face_mesh#refine_landmark.
|
||||
*
|
||||
* <p>minDetectionConfidence: Minimum confidence value ([0.0, 1.0]) for face detection to be
|
||||
* considered successful. See details in
|
||||
* https://solutions.mediapipe.dev/face_mesh#min_detection_confidence.
|
||||
@@ -39,19 +43,7 @@ import com.google.auto.value.AutoValue;
|
||||
@AutoValue
|
||||
public abstract class FaceMeshOptions {
|
||||
|
||||
// TODO: Switch to use boolean variable.
|
||||
public static final int STREAMING_MODE = 1;
|
||||
public static final int STATIC_IMAGE_MODE = 2;
|
||||
|
||||
/**
|
||||
* Indicates whether to treat the input images as a batch of static and possibly unrelated images,
|
||||
* or a video stream.
|
||||
*/
|
||||
@IntDef({STREAMING_MODE, STATIC_IMAGE_MODE})
|
||||
public @interface Mode {}
|
||||
|
||||
@Mode
|
||||
public abstract int mode();
|
||||
public abstract boolean staticImageMode();
|
||||
|
||||
public abstract int maxNumFaces();
|
||||
|
||||
@@ -59,6 +51,8 @@ public abstract class FaceMeshOptions {
|
||||
|
||||
public abstract float minTrackingConfidence();
|
||||
|
||||
public abstract boolean refineLandmarks();
|
||||
|
||||
public abstract boolean runOnGpu();
|
||||
|
||||
public static Builder builder() {
|
||||
@@ -69,13 +63,15 @@ public abstract class FaceMeshOptions {
|
||||
@AutoValue.Builder
|
||||
public abstract static class Builder {
|
||||
public Builder withDefaultValues() {
|
||||
return setMaxNumFaces(1)
|
||||
return setStaticImageMode(false)
|
||||
.setMaxNumFaces(1)
|
||||
.setMinDetectionConfidence(0.5f)
|
||||
.setMinTrackingConfidence(0.5f)
|
||||
.setRefineLandmarks(false)
|
||||
.setRunOnGpu(true);
|
||||
}
|
||||
|
||||
public abstract Builder setMode(int value);
|
||||
public abstract Builder setStaticImageMode(boolean value);
|
||||
|
||||
public abstract Builder setMaxNumFaces(int value);
|
||||
|
||||
@@ -83,6 +79,8 @@ public abstract class FaceMeshOptions {
|
||||
|
||||
public abstract Builder setMinTrackingConfidence(float value);
|
||||
|
||||
public abstract Builder setRefineLandmarks(boolean value);
|
||||
|
||||
public abstract Builder setRunOnGpu(boolean value);
|
||||
|
||||
public abstract FaceMeshOptions build();
|
||||
|
||||
@@ -79,15 +79,16 @@ public class Hands extends ImageSolutionBase {
|
||||
Connection.create(HandLandmark.PINKY_DIP, HandLandmark.PINKY_TIP));
|
||||
|
||||
private static final String NUM_HANDS = "num_hands";
|
||||
private static final String USE_PREV_LANDMARKS = "use_prev_landmarks";
|
||||
private static final String GPU_GRAPH_NAME = "hand_landmark_tracking_gpu_image.binarypb";
|
||||
private static final String CPU_GRAPH_NAME = "hand_landmark_tracking_cpu_image.binarypb";
|
||||
private static final String IMAGE_INPUT_STREAM = "image";
|
||||
private static final ImmutableList<String> OUTPUT_STREAMS =
|
||||
ImmutableList.of("multi_hand_landmarks", "multi_handedness", "image");
|
||||
ImmutableList.of("multi_hand_landmarks", "multi_handedness", "throttled_image");
|
||||
private static final int LANDMARKS_INDEX = 0;
|
||||
private static final int HANDEDNESS_INDEX = 1;
|
||||
private static final int INPUT_IMAGE_INDEX = 2;
|
||||
private final OutputHandler<HandsResult> graphOutputHandler;
|
||||
private final OutputHandler<HandsResult> outputHandler;
|
||||
|
||||
/**
|
||||
* Initializes MediaPipe Hands solution.
|
||||
@@ -96,21 +97,21 @@ public class Hands extends ImageSolutionBase {
|
||||
* @param options the configuration options defined in {@link HandsOptions}.
|
||||
*/
|
||||
public Hands(Context context, HandsOptions options) {
|
||||
graphOutputHandler = new OutputHandler<>();
|
||||
graphOutputHandler.setOutputConverter(
|
||||
outputHandler = new OutputHandler<>();
|
||||
outputHandler.setOutputConverter(
|
||||
packets -> {
|
||||
HandsResult.Builder handsResultBuilder = HandsResult.builder();
|
||||
try {
|
||||
handsResultBuilder.setMultiHandLandmarks(
|
||||
getProtoVector(packets.get(LANDMARKS_INDEX), NormalizedLandmarkList.parser()));
|
||||
} catch (MediaPipeException e) {
|
||||
throwException("Error occurs when getting MediaPipe hand landmarks. ", e);
|
||||
reportError("Error occurs while getting MediaPipe hand landmarks.", e);
|
||||
}
|
||||
try {
|
||||
handsResultBuilder.setMultiHandedness(
|
||||
getProtoVector(packets.get(HANDEDNESS_INDEX), Classification.parser()));
|
||||
} catch (MediaPipeException e) {
|
||||
throwException("Error occurs when getting MediaPipe handedness data. ", e);
|
||||
reportError("Error occurs while getting MediaPipe handedness data.", e);
|
||||
}
|
||||
return handsResultBuilder
|
||||
.setImagePacket(packets.get(INPUT_IMAGE_INDEX))
|
||||
@@ -124,22 +125,23 @@ public class Hands extends ImageSolutionBase {
|
||||
.setBinaryGraphPath(options.runOnGpu() ? GPU_GRAPH_NAME : CPU_GRAPH_NAME)
|
||||
.setImageInputStreamName(IMAGE_INPUT_STREAM)
|
||||
.setOutputStreamNames(OUTPUT_STREAMS)
|
||||
.setStaticImageMode(options.mode() == HandsOptions.STATIC_IMAGE_MODE)
|
||||
.setStaticImageMode(options.staticImageMode())
|
||||
.build();
|
||||
|
||||
initialize(context, solutionInfo, graphOutputHandler);
|
||||
initialize(context, solutionInfo, outputHandler);
|
||||
Map<String, Packet> inputSidePackets = new HashMap<>();
|
||||
inputSidePackets.put(NUM_HANDS, packetCreator.createInt32(options.maxNumHands()));
|
||||
inputSidePackets.put(USE_PREV_LANDMARKS, packetCreator.createBool(!options.staticImageMode()));
|
||||
start(inputSidePackets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a callback to be invoked when the HandsResults become available.
|
||||
* Sets a callback to be invoked when a {@link HandsResult} becomes available.
|
||||
*
|
||||
* @param listener the {@link ResultListener} callback.
|
||||
*/
|
||||
public void setResultListener(ResultListener<HandsResult> listener) {
|
||||
this.graphOutputHandler.setResultListener(listener);
|
||||
this.outputHandler.setResultListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,7 +150,7 @@ public class Hands extends ImageSolutionBase {
|
||||
* @param listener the {@link ErrorListener} callback.
|
||||
*/
|
||||
public void setErrorListener(@Nullable ErrorListener listener) {
|
||||
this.graphOutputHandler.setErrorListener(listener);
|
||||
this.outputHandler.setErrorListener(listener);
|
||||
this.errorListener = listener;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,14 +14,14 @@
|
||||
|
||||
package com.google.mediapipe.solutions.hands;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import com.google.auto.value.AutoValue;
|
||||
|
||||
/**
|
||||
* MediaPipe Hands solution-specific options.
|
||||
*
|
||||
* <p>mode: Whether to treat the input images as a batch of static and possibly unrelated images, or
|
||||
* a video stream. See details in https://solutions.mediapipe.dev/hands#static_image_mode.
|
||||
* <p>staticImageMode: Whether to treat the input images as a batch of static and possibly unrelated
|
||||
* images, or a video stream. Default to false. See details in
|
||||
* https://solutions.mediapipe.dev/hands#static_image_mode.
|
||||
*
|
||||
* <p>maxNumHands: Maximum number of hands to detect. See details in
|
||||
* https://solutions.mediapipe.dev/hands#max_num_hands.
|
||||
@@ -39,19 +39,7 @@ import com.google.auto.value.AutoValue;
|
||||
@AutoValue
|
||||
public abstract class HandsOptions {
|
||||
|
||||
// TODO: Switch to use boolean variable.
|
||||
public static final int STREAMING_MODE = 1;
|
||||
public static final int STATIC_IMAGE_MODE = 2;
|
||||
|
||||
/**
|
||||
* Indicates whether to treat the input images as a batch of static and possibly unrelated images,
|
||||
* or a video stream.
|
||||
*/
|
||||
@IntDef({STREAMING_MODE, STATIC_IMAGE_MODE})
|
||||
public @interface Mode {}
|
||||
|
||||
@Mode
|
||||
public abstract int mode();
|
||||
public abstract boolean staticImageMode();
|
||||
|
||||
public abstract int maxNumHands();
|
||||
|
||||
@@ -69,13 +57,14 @@ public abstract class HandsOptions {
|
||||
@AutoValue.Builder
|
||||
public abstract static class Builder {
|
||||
public Builder withDefaultValues() {
|
||||
return setMaxNumHands(2)
|
||||
return setStaticImageMode(false)
|
||||
.setMaxNumHands(2)
|
||||
.setMinDetectionConfidence(0.5f)
|
||||
.setMinTrackingConfidence(0.5f)
|
||||
.setRunOnGpu(true);
|
||||
}
|
||||
|
||||
public abstract Builder setMode(int value);
|
||||
public abstract Builder setStaticImageMode(boolean value);
|
||||
|
||||
public abstract Builder setMaxNumHands(int value);
|
||||
|
||||
|
||||
@@ -117,6 +117,30 @@ mediapipe_simple_subgraph(
|
||||
],
|
||||
)
|
||||
|
||||
mediapipe_simple_subgraph(
|
||||
name = "face_detection_short_range_image",
|
||||
graph = "face_detection_short_range_image.pbtxt",
|
||||
register_as = "FaceDetectionShortRangeImage",
|
||||
deps = [
|
||||
":face_detection_short_range_common",
|
||||
"//mediapipe/calculators/core:flow_limiter_calculator",
|
||||
"//mediapipe/calculators/tensor:image_to_tensor_calculator",
|
||||
"//mediapipe/calculators/tensor:inference_calculator",
|
||||
],
|
||||
)
|
||||
|
||||
mediapipe_simple_subgraph(
|
||||
name = "face_detection_full_range_image",
|
||||
graph = "face_detection_full_range_image.pbtxt",
|
||||
register_as = "FaceDetectionFullRangeImage",
|
||||
deps = [
|
||||
":face_detection_full_range_common",
|
||||
"//mediapipe/calculators/core:flow_limiter_calculator",
|
||||
"//mediapipe/calculators/tensor:image_to_tensor_calculator",
|
||||
"//mediapipe/calculators/tensor:inference_calculator",
|
||||
],
|
||||
)
|
||||
|
||||
exports_files(
|
||||
srcs = [
|
||||
"face_detection_full_range.tflite",
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# MediaPipe graph to detect faces. (GPU/CPU input, and inference is executed on
|
||||
# GPU.)
|
||||
#
|
||||
# It is required that "face_detection_full_range_sparse.tflite" is available at
|
||||
# "mediapipe/modules/face_detection/face_detection_full_range_sparse.tflite"
|
||||
# path during execution.
|
||||
|
||||
type: "FaceDetectionFullRangeImage"
|
||||
|
||||
# Image. (Image)
|
||||
input_stream: "IMAGE:image"
|
||||
|
||||
# The throttled input image. (Image)
|
||||
output_stream: "IMAGE:throttled_image"
|
||||
# Detected faces. (std::vector<Detection>)
|
||||
# NOTE: there will not be an output packet in the DETECTIONS stream for this
|
||||
# particular timestamp if none of faces detected. However, the MediaPipe
|
||||
# framework will internally inform the downstream calculators of the absence of
|
||||
# this packet so that they don't wait for it unnecessarily.
|
||||
output_stream: "DETECTIONS:detections"
|
||||
|
||||
node {
|
||||
calculator: "FlowLimiterCalculator"
|
||||
input_stream: "image"
|
||||
input_stream: "FINISHED:detections"
|
||||
input_stream_info: {
|
||||
tag_index: "FINISHED"
|
||||
back_edge: true
|
||||
}
|
||||
output_stream: "throttled_image"
|
||||
options: {
|
||||
[mediapipe.FlowLimiterCalculatorOptions.ext] {
|
||||
max_in_flight: 1
|
||||
max_in_queue: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Transforms the input image into a 128x128 tensor while keeping the aspect
|
||||
# ratio (what is expected by the corresponding face detection model), resulting
|
||||
# in potential letterboxing in the transformed image.
|
||||
node: {
|
||||
calculator: "ImageToTensorCalculator"
|
||||
input_stream: "IMAGE:throttled_image"
|
||||
output_stream: "TENSORS:input_tensors"
|
||||
output_stream: "MATRIX:transform_matrix"
|
||||
options: {
|
||||
[mediapipe.ImageToTensorCalculatorOptions.ext] {
|
||||
output_tensor_width: 192
|
||||
output_tensor_height: 192
|
||||
keep_aspect_ratio: true
|
||||
output_tensor_float_range {
|
||||
min: -1.0
|
||||
max: 1.0
|
||||
}
|
||||
border_mode: BORDER_ZERO
|
||||
gpu_origin: CONVENTIONAL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 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.
|
||||
# TODO: Use GraphOptions to modify the delegate field to be
|
||||
# `delegate { xnnpack {} }` for the CPU only use cases.
|
||||
node {
|
||||
calculator: "InferenceCalculator"
|
||||
input_stream: "TENSORS:input_tensors"
|
||||
output_stream: "TENSORS:detection_tensors"
|
||||
options: {
|
||||
[mediapipe.InferenceCalculatorOptions.ext] {
|
||||
model_path: "mediapipe/modules/face_detection/face_detection_full_range_sparse.tflite"
|
||||
#
|
||||
delegate: { gpu { use_advanced_gpu_api: true } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Performs tensor post processing to generate face detections.
|
||||
node {
|
||||
calculator: "FaceDetectionFullRangeCommon"
|
||||
input_stream: "TENSORS:detection_tensors"
|
||||
input_stream: "MATRIX:transform_matrix"
|
||||
output_stream: "DETECTIONS:detections"
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
# MediaPipe graph to detect faces. (GPU/CPU input, and inference is executed on
|
||||
# GPU.)
|
||||
#
|
||||
# It is required that "face_detection_short_range.tflite" is available at
|
||||
# "mediapipe/modules/face_detection/face_detection_short_range.tflite"
|
||||
# path during execution.
|
||||
#
|
||||
# EXAMPLE:
|
||||
# node {
|
||||
# calculator: "FaceDetectionShortRangeCpu"
|
||||
# input_stream: "IMAGE:image"
|
||||
# output_stream: "DETECTIONS:face_detections"
|
||||
# }
|
||||
|
||||
type: "FaceDetectionShortRangeCpu"
|
||||
|
||||
# Image. (Image)
|
||||
input_stream: "IMAGE:image"
|
||||
|
||||
# The throttled input image. (Image)
|
||||
output_stream: "IMAGE:throttled_image"
|
||||
# Detected faces. (std::vector<Detection>)
|
||||
# NOTE: there will not be an output packet in the DETECTIONS stream for this
|
||||
# particular timestamp if none of faces detected. However, the MediaPipe
|
||||
# framework will internally inform the downstream calculators of the absence of
|
||||
# this packet so that they don't wait for it unnecessarily.
|
||||
output_stream: "DETECTIONS:detections"
|
||||
|
||||
node {
|
||||
calculator: "FlowLimiterCalculator"
|
||||
input_stream: "image"
|
||||
input_stream: "FINISHED:detections"
|
||||
input_stream_info: {
|
||||
tag_index: "FINISHED"
|
||||
back_edge: true
|
||||
}
|
||||
output_stream: "throttled_image"
|
||||
options: {
|
||||
[mediapipe.FlowLimiterCalculatorOptions.ext] {
|
||||
max_in_flight: 1
|
||||
max_in_queue: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Transforms the input image into a 128x128 tensor while keeping the aspect
|
||||
# ratio (what is expected by the corresponding face detection model), resulting
|
||||
# in potential letterboxing in the transformed image.
|
||||
node: {
|
||||
calculator: "ImageToTensorCalculator"
|
||||
input_stream: "IMAGE:throttled_image"
|
||||
output_stream: "TENSORS:input_tensors"
|
||||
output_stream: "MATRIX:transform_matrix"
|
||||
options: {
|
||||
[mediapipe.ImageToTensorCalculatorOptions.ext] {
|
||||
output_tensor_width: 128
|
||||
output_tensor_height: 128
|
||||
keep_aspect_ratio: true
|
||||
output_tensor_float_range {
|
||||
min: -1.0
|
||||
max: 1.0
|
||||
}
|
||||
border_mode: BORDER_ZERO
|
||||
gpu_origin: CONVENTIONAL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 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.
|
||||
# TODO: Use GraphOptions to modify the delegate field to be
|
||||
# `delegate { xnnpack {} }` for the CPU only use cases.
|
||||
node {
|
||||
calculator: "InferenceCalculator"
|
||||
input_stream: "TENSORS:input_tensors"
|
||||
output_stream: "TENSORS:detection_tensors"
|
||||
options: {
|
||||
[mediapipe.InferenceCalculatorOptions.ext] {
|
||||
model_path: "mediapipe/modules/face_detection/face_detection_short_range.tflite"
|
||||
|
||||
#
|
||||
delegate: { gpu { use_advanced_gpu_api: true } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Performs tensor post processing to generate face detections.
|
||||
node {
|
||||
calculator: "FaceDetectionShortRangeCommon"
|
||||
input_stream: "TENSORS:detection_tensors"
|
||||
input_stream: "MATRIX:transform_matrix"
|
||||
output_stream: "DETECTIONS:detections"
|
||||
}
|
||||
@@ -26,14 +26,19 @@ mediapipe_simple_subgraph(
|
||||
graph = "face_landmark_cpu.pbtxt",
|
||||
register_as = "FaceLandmarkCpu",
|
||||
deps = [
|
||||
":face_landmarks_model_loader",
|
||||
":tensors_to_face_landmarks",
|
||||
":tensors_to_face_landmarks_with_attention",
|
||||
"//mediapipe/calculators/core:gate_calculator",
|
||||
"//mediapipe/calculators/core:split_vector_calculator",
|
||||
"//mediapipe/calculators/tensor:image_to_tensor_calculator",
|
||||
"//mediapipe/calculators/tensor:inference_calculator",
|
||||
"//mediapipe/calculators/tensor:tensors_to_floats_calculator",
|
||||
"//mediapipe/calculators/tensor:tensors_to_landmarks_calculator",
|
||||
"//mediapipe/calculators/tflite:tflite_custom_op_resolver_calculator",
|
||||
"//mediapipe/calculators/util:landmark_projection_calculator",
|
||||
"//mediapipe/calculators/util:thresholding_calculator",
|
||||
"//mediapipe/framework/tool:switch_container",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -42,14 +47,19 @@ mediapipe_simple_subgraph(
|
||||
graph = "face_landmark_gpu.pbtxt",
|
||||
register_as = "FaceLandmarkGpu",
|
||||
deps = [
|
||||
":face_landmarks_model_loader",
|
||||
":tensors_to_face_landmarks",
|
||||
":tensors_to_face_landmarks_with_attention",
|
||||
"//mediapipe/calculators/core:gate_calculator",
|
||||
"//mediapipe/calculators/core:split_vector_calculator",
|
||||
"//mediapipe/calculators/tensor:image_to_tensor_calculator",
|
||||
"//mediapipe/calculators/tensor:inference_calculator",
|
||||
"//mediapipe/calculators/tensor:tensors_to_floats_calculator",
|
||||
"//mediapipe/calculators/tensor:tensors_to_landmarks_calculator",
|
||||
"//mediapipe/calculators/tflite:tflite_custom_op_resolver_calculator",
|
||||
"//mediapipe/calculators/util:landmark_projection_calculator",
|
||||
"//mediapipe/calculators/util:thresholding_calculator",
|
||||
"//mediapipe/framework/tool:switch_container",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -101,6 +111,7 @@ mediapipe_simple_subgraph(
|
||||
register_as = "FaceLandmarkFrontCpuImage",
|
||||
deps = [
|
||||
":face_landmark_front_cpu",
|
||||
"//mediapipe/calculators/core:flow_limiter_calculator",
|
||||
"//mediapipe/calculators/image:image_transformation_calculator",
|
||||
"//mediapipe/calculators/util:from_image_calculator",
|
||||
],
|
||||
@@ -112,6 +123,7 @@ mediapipe_simple_subgraph(
|
||||
register_as = "FaceLandmarkFrontGpuImage",
|
||||
deps = [
|
||||
":face_landmark_front_gpu",
|
||||
"//mediapipe/calculators/core:flow_limiter_calculator",
|
||||
"//mediapipe/calculators/image:image_transformation_calculator",
|
||||
"//mediapipe/calculators/util:from_image_calculator",
|
||||
],
|
||||
@@ -120,6 +132,7 @@ mediapipe_simple_subgraph(
|
||||
exports_files(
|
||||
srcs = [
|
||||
"face_landmark.tflite",
|
||||
"face_landmark_with_attention.tflite",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -143,3 +156,35 @@ mediapipe_simple_subgraph(
|
||||
"//mediapipe/calculators/util:rect_transformation_calculator",
|
||||
],
|
||||
)
|
||||
|
||||
mediapipe_simple_subgraph(
|
||||
name = "face_landmarks_model_loader",
|
||||
graph = "face_landmarks_model_loader.pbtxt",
|
||||
register_as = "FaceLandmarksModelLoader",
|
||||
deps = [
|
||||
"//mediapipe/calculators/core:constant_side_packet_calculator",
|
||||
"//mediapipe/calculators/tflite:tflite_model_calculator",
|
||||
"//mediapipe/calculators/util:local_file_contents_calculator",
|
||||
"//mediapipe/framework/tool:switch_container",
|
||||
],
|
||||
)
|
||||
|
||||
mediapipe_simple_subgraph(
|
||||
name = "tensors_to_face_landmarks",
|
||||
graph = "tensors_to_face_landmarks.pbtxt",
|
||||
register_as = "TensorsToFaceLandmarks",
|
||||
deps = [
|
||||
"//mediapipe/calculators/tensor:tensors_to_landmarks_calculator",
|
||||
],
|
||||
)
|
||||
|
||||
mediapipe_simple_subgraph(
|
||||
name = "tensors_to_face_landmarks_with_attention",
|
||||
graph = "tensors_to_face_landmarks_with_attention.pbtxt",
|
||||
register_as = "TensorsToFaceLandmarksWithAttention",
|
||||
deps = [
|
||||
"//mediapipe/calculators/core:split_vector_calculator",
|
||||
"//mediapipe/calculators/tensor:tensors_to_landmarks_calculator",
|
||||
"//mediapipe/calculators/util:landmarks_refinement_calculator",
|
||||
],
|
||||
)
|
||||
|
||||
Binary file not shown.
@@ -3,13 +3,18 @@
|
||||
#
|
||||
# It is required that "face_landmark.tflite" is available at
|
||||
# "mediapipe/modules/face_landmark/face_landmark.tflite"
|
||||
# path during execution.
|
||||
# path during execution if `with_attention` is not set or set to `false`.
|
||||
#
|
||||
# It is required that "face_landmark_with_attention.tflite" is available at
|
||||
# "mediapipe/modules/face_landmark/face_landmark_with_attention.tflite"
|
||||
# path during execution if `with_attention` is set to `true`.
|
||||
#
|
||||
# EXAMPLE:
|
||||
# node {
|
||||
# calculator: "FaceLandmarkCpu"
|
||||
# input_stream: "IMAGE:image"
|
||||
# input_stream: "ROI:face_roi"
|
||||
# input_side_packet: "WITH_ATTENTION:with_attention"
|
||||
# output_stream: "LANDMARKS:face_landmarks"
|
||||
# }
|
||||
|
||||
@@ -20,8 +25,17 @@ input_stream: "IMAGE:image"
|
||||
# ROI (region of interest) within the given image where a face is located.
|
||||
# (NormalizedRect)
|
||||
input_stream: "ROI:roi"
|
||||
# Whether to run face mesh model with attention on lips and eyes. (bool)
|
||||
# Attention provides more accuracy on lips and eye regions as well as iris
|
||||
# landmarks.
|
||||
input_side_packet: "WITH_ATTENTION:with_attention"
|
||||
|
||||
# 468 face landmarks within the given ROI. (NormalizedLandmarkList)
|
||||
# 468 or 478 facial landmarks within the given ROI. (NormalizedLandmarkList)
|
||||
#
|
||||
# Number of landmarks depends on the WITH_ATTENTION flag. If it's `true` - then
|
||||
# there will be 478 landmarks with refined lips, eyes and irises (10 extra
|
||||
# landmarks are for irises), otherwise 468 non-refined landmarks are returned.
|
||||
#
|
||||
# NOTE: if a face is not present within the given ROI, for this particular
|
||||
# timestamp there will not be an output packet in the LANDMARKS stream. However,
|
||||
# the MediaPipe framework will internally inform the downstream calculators of
|
||||
@@ -46,31 +60,63 @@ node: {
|
||||
}
|
||||
}
|
||||
|
||||
# Loads the face landmarks TF Lite model.
|
||||
node {
|
||||
calculator: "FaceLandmarksModelLoader"
|
||||
input_side_packet: "WITH_ATTENTION:with_attention"
|
||||
output_side_packet: "MODEL:model"
|
||||
}
|
||||
|
||||
# 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"
|
||||
}
|
||||
|
||||
# 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: "InferenceCalculator"
|
||||
input_stream: "TENSORS:input_tensors"
|
||||
input_side_packet: "MODEL:model"
|
||||
input_side_packet: "CUSTOM_OP_RESOLVER:op_resolver"
|
||||
output_stream: "TENSORS:output_tensors"
|
||||
options: {
|
||||
[mediapipe.InferenceCalculatorOptions.ext] {
|
||||
model_path: "mediapipe/modules/face_landmark/face_landmark.tflite"
|
||||
delegate { xnnpack {} }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Splits a vector of tensors into multiple vectors.
|
||||
# Splits a vector of tensors into landmark tensors and face flag tensor.
|
||||
node {
|
||||
calculator: "SplitTensorVectorCalculator"
|
||||
calculator: "SwitchContainer"
|
||||
input_side_packet: "ENABLE:with_attention"
|
||||
input_stream: "output_tensors"
|
||||
output_stream: "landmark_tensors"
|
||||
output_stream: "face_flag_tensor"
|
||||
options: {
|
||||
[mediapipe.SplitVectorCalculatorOptions.ext] {
|
||||
ranges: { begin: 0 end: 1 }
|
||||
ranges: { begin: 1 end: 2 }
|
||||
[mediapipe.SwitchContainerOptions.ext] {
|
||||
contained_node: {
|
||||
calculator: "SplitTensorVectorCalculator"
|
||||
options: {
|
||||
[mediapipe.SplitVectorCalculatorOptions.ext] {
|
||||
ranges: { begin: 0 end: 1 }
|
||||
ranges: { begin: 1 end: 2 }
|
||||
}
|
||||
}
|
||||
}
|
||||
contained_node: {
|
||||
calculator: "SplitTensorVectorCalculator"
|
||||
options: {
|
||||
[mediapipe.SplitVectorCalculatorOptions.ext] {
|
||||
ranges: { begin: 0 end: 6 }
|
||||
ranges: { begin: 6 end: 7 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,14 +158,18 @@ node {
|
||||
# Decodes the landmark tensors into a vector of landmarks, where the landmark
|
||||
# coordinates are normalized by the size of the input image to the model.
|
||||
node {
|
||||
calculator: "TensorsToLandmarksCalculator"
|
||||
calculator: "SwitchContainer"
|
||||
input_side_packet: "ENABLE:with_attention"
|
||||
input_stream: "TENSORS:ensured_landmark_tensors"
|
||||
output_stream: "NORM_LANDMARKS:landmarks"
|
||||
output_stream: "LANDMARKS:landmarks"
|
||||
options: {
|
||||
[mediapipe.TensorsToLandmarksCalculatorOptions.ext] {
|
||||
num_landmarks: 468
|
||||
input_image_width: 192
|
||||
input_image_height: 192
|
||||
[mediapipe.SwitchContainerOptions.ext] {
|
||||
contained_node: {
|
||||
calculator: "TensorsToFaceLandmarks"
|
||||
}
|
||||
contained_node: {
|
||||
calculator: "TensorsToFaceLandmarksWithAttention"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,19 @@
|
||||
#
|
||||
# It is required that "face_landmark.tflite" is available at
|
||||
# "mediapipe/modules/face_landmark/face_landmark.tflite"
|
||||
# path during execution.
|
||||
# path during execution if `with_attention` is not set or set to `false`.
|
||||
#
|
||||
# It is required that "face_landmark_with_attention.tflite" is available at
|
||||
# "mediapipe/modules/face_landmark/face_landmark_with_attention.tflite"
|
||||
# path during execution if `with_attention` is set to `true`.
|
||||
#
|
||||
# EXAMPLE:
|
||||
# node {
|
||||
# calculator: "FaceLandmarkFrontCpu"
|
||||
# input_stream: "IMAGE:image"
|
||||
# input_side_packet: "NUM_FACES:num_faces"
|
||||
# input_side_packet: "USE_PREV_LANDMARKS:use_prev_landmarks"
|
||||
# input_side_packet: "WITH_ATTENTION:with_attention"
|
||||
# output_stream: "LANDMARKS:multi_face_landmarks"
|
||||
# }
|
||||
|
||||
@@ -26,6 +32,15 @@ input_stream: "IMAGE:image"
|
||||
# Max number of faces to detect/track. (int)
|
||||
input_side_packet: "NUM_FACES:num_faces"
|
||||
|
||||
# Whether landmarks on the previous image should be used to help localize
|
||||
# landmarks on the current image. (bool)
|
||||
input_side_packet: "USE_PREV_LANDMARKS:use_prev_landmarks"
|
||||
|
||||
# Whether to run face mesh model with attention on lips and eyes. (bool)
|
||||
# Attention provides more accuracy on lips and eye regions as well as iris
|
||||
# landmarks.
|
||||
input_side_packet: "WITH_ATTENTION:with_attention"
|
||||
|
||||
# Collection of detected/predicted faces, each represented as a list of 468 face
|
||||
# landmarks. (std::vector<NormalizedLandmarkList>)
|
||||
# NOTE: there will not be an output packet in the LANDMARKS stream for this
|
||||
@@ -44,23 +59,19 @@ output_stream: "ROIS_FROM_LANDMARKS:face_rects_from_landmarks"
|
||||
# (std::vector<NormalizedRect>)
|
||||
output_stream: "ROIS_FROM_DETECTIONS:face_rects_from_detections"
|
||||
|
||||
# Defines whether landmarks on the previous image should be used to help
|
||||
# localize landmarks on the current image.
|
||||
node {
|
||||
name: "ConstantSidePacketCalculator"
|
||||
calculator: "ConstantSidePacketCalculator"
|
||||
output_side_packet: "PACKET:use_prev_landmarks"
|
||||
options: {
|
||||
[mediapipe.ConstantSidePacketCalculatorOptions.ext]: {
|
||||
packet { bool_value: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
# When the optional input side packet "use_prev_landmarks" is either absent or
|
||||
# set to true, uses the landmarks on the previous image to help localize
|
||||
# landmarks on the current image.
|
||||
node {
|
||||
calculator: "GateCalculator"
|
||||
input_side_packet: "ALLOW:use_prev_landmarks"
|
||||
input_stream: "prev_face_rects_from_landmarks"
|
||||
output_stream: "gated_prev_face_rects_from_landmarks"
|
||||
options: {
|
||||
[mediapipe.GateCalculatorOptions.ext] {
|
||||
allow: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Determines if an input vector of NormalizedRect has a size greater than or
|
||||
@@ -186,6 +197,7 @@ node {
|
||||
calculator: "FaceLandmarkCpu"
|
||||
input_stream: "IMAGE:landmarks_loop_image"
|
||||
input_stream: "ROI:face_rect"
|
||||
input_side_packet: "WITH_ATTENTION:with_attention"
|
||||
output_stream: "LANDMARKS:face_landmarks"
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user