Project import generated by Copybara.
GitOrigin-RevId: f7d09ed033907b893638a8eb4148efa11c0f09a6
This commit is contained in:
+20
-8
@@ -16,14 +16,6 @@ load("@pybind11_bazel//:build_defs.bzl", "pybind_extension")
|
||||
|
||||
licenses(["notice"]) # Apache 2.0
|
||||
|
||||
cc_library(
|
||||
name = "builtin_calculators",
|
||||
deps = [
|
||||
"//mediapipe/calculators/core:pass_through_calculator",
|
||||
"//mediapipe/graphs/pose_tracking:upper_body_pose_tracking_cpu_deps",
|
||||
],
|
||||
)
|
||||
|
||||
pybind_extension(
|
||||
name = "_framework_bindings",
|
||||
srcs = ["framework_bindings.cc"],
|
||||
@@ -50,5 +42,25 @@ pybind_extension(
|
||||
"//mediapipe/python/pybind:resource_util",
|
||||
"//mediapipe/python/pybind:timestamp",
|
||||
"//mediapipe/python/pybind:validated_graph_config",
|
||||
# Type registration.
|
||||
"//mediapipe/framework:basic_types_registration",
|
||||
"//mediapipe/framework/formats:classification_registration",
|
||||
"//mediapipe/framework/formats:detection_registration",
|
||||
"//mediapipe/framework/formats:landmark_registration",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "builtin_calculators",
|
||||
deps = [
|
||||
"//mediapipe/calculators/core:pass_through_calculator",
|
||||
"//mediapipe/calculators/core:split_normalized_landmark_list_calculator",
|
||||
"//mediapipe/modules/face_detection:face_detection_front_cpu",
|
||||
"//mediapipe/modules/face_landmark:face_landmark_front_cpu",
|
||||
"//mediapipe/modules/hand_landmark:hand_landmark_tracking_cpu",
|
||||
"//mediapipe/modules/palm_detection:palm_detection_cpu",
|
||||
"//mediapipe/modules/pose_detection:pose_detection_cpu",
|
||||
"//mediapipe/modules/pose_landmark:pose_landmark_upper_body_by_roi_cpu",
|
||||
"//mediapipe/modules/pose_landmark:pose_landmark_upper_body_smoothed_cpu",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
namespace mediapipe {
|
||||
namespace python {
|
||||
|
||||
// A mutex to guard the output stream observer python callback function.
|
||||
// Only one python callback can run at once.
|
||||
absl::Mutex callback_mutex;
|
||||
|
||||
template <typename T>
|
||||
T ParseProto(const py::object& proto_object) {
|
||||
T proto;
|
||||
@@ -393,6 +397,8 @@ void CalculatorGraphSubmodule(pybind11::module* module) {
|
||||
pybind11::function callback_fn) {
|
||||
RaisePyErrorIfNotOk(self->ObserveOutputStream(
|
||||
stream_name, [callback_fn, stream_name](const Packet& packet) {
|
||||
// Acquire a mutex so that only one callback_fn can run at once.
|
||||
absl::MutexLock lock(&callback_mutex);
|
||||
callback_fn(stream_name, packet);
|
||||
return mediapipe::OkStatus();
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,472 @@
|
||||
# Copyright 2020 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.
|
||||
|
||||
# Lint as: python3
|
||||
"""MediaPipe SolutionBase module.
|
||||
|
||||
MediaPipe SolutionBase is the common base class for the high-level MediaPipe
|
||||
Solution APIs such as BlazeFace, hand tracking, and BlazePose. The SolutionBase
|
||||
class contains the shared logic among the high-level Solution APIs including
|
||||
graph initialization, processing image/audio data, and graph shutdown. Thus,
|
||||
users can easily create new MediaPipe Solution APIs on top of the SolutionBase
|
||||
class.
|
||||
"""
|
||||
|
||||
import collections
|
||||
import enum
|
||||
import os
|
||||
from typing import Any, Iterable, List, Mapping, NamedTuple, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from google.protobuf import descriptor
|
||||
# resources dependency
|
||||
from mediapipe.framework import calculator_pb2
|
||||
# pylint: disable=unused-import
|
||||
from mediapipe.framework.formats import detection_pb2
|
||||
from mediapipe.calculators.image import image_transformation_calculator_pb2
|
||||
from mediapipe.calculators.tensor import tensors_to_detections_calculator_pb2
|
||||
from mediapipe.calculators.util import landmarks_smoothing_calculator_pb2
|
||||
from mediapipe.calculators.util import logic_calculator_pb2
|
||||
from mediapipe.calculators.util import thresholding_calculator_pb2
|
||||
from mediapipe.framework.formats import classification_pb2
|
||||
from mediapipe.framework.formats import landmark_pb2
|
||||
from mediapipe.framework.formats import rect_pb2
|
||||
# pylint: enable=unused-import
|
||||
from mediapipe.python._framework_bindings import calculator_graph
|
||||
from mediapipe.python._framework_bindings import image_frame
|
||||
from mediapipe.python._framework_bindings import packet
|
||||
from mediapipe.python._framework_bindings import resource_util
|
||||
from mediapipe.python._framework_bindings import validated_graph_config
|
||||
import mediapipe.python.packet_creator as packet_creator
|
||||
import mediapipe.python.packet_getter as packet_getter
|
||||
|
||||
RGB_CHANNELS = 3
|
||||
# TODO: Enable calculator options modification for more calculators.
|
||||
CALCULATOR_TO_OPTIONS = {
|
||||
'ImageTransformationCalculator':
|
||||
image_transformation_calculator_pb2
|
||||
.ImageTransformationCalculatorOptions,
|
||||
'LandmarksSmoothingCalculator':
|
||||
landmarks_smoothing_calculator_pb2.LandmarksSmoothingCalculatorOptions,
|
||||
'LogicCalculator':
|
||||
logic_calculator_pb2.LogicCalculatorOptions,
|
||||
'ThresholdingCalculator':
|
||||
thresholding_calculator_pb2.ThresholdingCalculatorOptions,
|
||||
'TensorsToDetectionsCalculator':
|
||||
tensors_to_detections_calculator_pb2
|
||||
.TensorsToDetectionsCalculatorOptions,
|
||||
}
|
||||
|
||||
|
||||
# TODO: Support more packet data types, such as "Any" type.
|
||||
@enum.unique
|
||||
class _PacketDataType(enum.Enum):
|
||||
"""The packet data types supported by the SolutionBase class."""
|
||||
STRING = 'string'
|
||||
BOOL = 'bool'
|
||||
INT = 'int'
|
||||
FLOAT = 'float'
|
||||
AUDIO = 'matrix'
|
||||
IMAGE = 'image_frame'
|
||||
PROTO = 'proto'
|
||||
PROTO_LIST = 'proto_list'
|
||||
|
||||
@staticmethod
|
||||
def from_registered_name(registered_name: str) -> '_PacketDataType':
|
||||
return NAME_TO_TYPE[registered_name]
|
||||
|
||||
|
||||
NAME_TO_TYPE: Mapping[str, '_PacketDataType'] = {
|
||||
'string':
|
||||
_PacketDataType.STRING,
|
||||
'bool':
|
||||
_PacketDataType.BOOL,
|
||||
'int':
|
||||
_PacketDataType.INT,
|
||||
'float':
|
||||
_PacketDataType.FLOAT,
|
||||
'::mediapipe::Matrix':
|
||||
_PacketDataType.AUDIO,
|
||||
'::mediapipe::ImageFrame':
|
||||
_PacketDataType.IMAGE,
|
||||
'::mediapipe::Classification':
|
||||
_PacketDataType.PROTO,
|
||||
'::mediapipe::ClassificationList':
|
||||
_PacketDataType.PROTO,
|
||||
'::mediapipe::Detection':
|
||||
_PacketDataType.PROTO,
|
||||
'::mediapipe::DetectionList':
|
||||
_PacketDataType.PROTO,
|
||||
'::mediapipe::Landmark':
|
||||
_PacketDataType.PROTO,
|
||||
'::mediapipe::NormalizedLandmark':
|
||||
_PacketDataType.PROTO,
|
||||
'::mediapipe::Rect':
|
||||
_PacketDataType.PROTO,
|
||||
'::mediapipe::NormalizedRect':
|
||||
_PacketDataType.PROTO,
|
||||
'::mediapipe::NormalizedLandmarkList':
|
||||
_PacketDataType.PROTO,
|
||||
'::std::vector<::mediapipe::Classification>':
|
||||
_PacketDataType.PROTO_LIST,
|
||||
'::std::vector<::mediapipe::ClassificationList>':
|
||||
_PacketDataType.PROTO_LIST,
|
||||
'::std::vector<::mediapipe::Detection>':
|
||||
_PacketDataType.PROTO_LIST,
|
||||
'::std::vector<::mediapipe::DetectionList>':
|
||||
_PacketDataType.PROTO_LIST,
|
||||
'::std::vector<::mediapipe::Landmark>':
|
||||
_PacketDataType.PROTO_LIST,
|
||||
'::std::vector<::mediapipe::NormalizedLandmark>':
|
||||
_PacketDataType.PROTO_LIST,
|
||||
'::std::vector<::mediapipe::NormalizedLandmarkList>':
|
||||
_PacketDataType.PROTO_LIST,
|
||||
'::std::vector<::mediapipe::Rect>':
|
||||
_PacketDataType.PROTO_LIST,
|
||||
'::std::vector<::mediapipe::NormalizedRect>':
|
||||
_PacketDataType.PROTO_LIST,
|
||||
}
|
||||
|
||||
|
||||
class SolutionBase:
|
||||
"""The common base class for the high-level MediaPipe Solution APIs.
|
||||
|
||||
The SolutionBase class contains the shared logic among the high-level solution
|
||||
APIs including graph initialization, processing image/audio data, and graph
|
||||
shutdown.
|
||||
|
||||
Example usage:
|
||||
hand_tracker = solution_base.SolutionBase(
|
||||
binary_graph_path='mediapipe/modules/hand_landmark/hand_landmark_tracking_cpu.binarypb',
|
||||
side_inputs={'num_hands': 2})
|
||||
# Read an image and convert the BGR image to RGB.
|
||||
input_image = cv2.cvtColor(cv2.imread('/tmp/hand.png'), COLOR_BGR2RGB)
|
||||
results = hand_tracker.process(input_image)
|
||||
print(results.palm_detections)
|
||||
print(results.multi_hand_landmarks)
|
||||
hand_tracker.close()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
binary_graph_path: Optional[str] = None,
|
||||
graph_config: Optional[calculator_pb2.CalculatorGraphConfig] = None,
|
||||
calculator_params: Optional[Mapping[str, Any]] = None,
|
||||
side_inputs: Optional[Mapping[str, Any]] = None,
|
||||
outputs: Optional[List[str]] = None):
|
||||
"""Initializes the SolutionBase object.
|
||||
|
||||
Args:
|
||||
binary_graph_path: The path to a binary mediapipe graph file (.binarypb).
|
||||
graph_config: A CalculatorGraphConfig proto message or its text proto
|
||||
format.
|
||||
calculator_params: A mapping from the
|
||||
{calculator_name}.{options_field_name} str to the field value.
|
||||
side_inputs: A mapping from the side packet name to the packet raw data.
|
||||
outputs: A list of the graph output stream names to observe. If the list
|
||||
is empty, all the output streams listed in the graph config will be
|
||||
automatically observed by default.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the binary graph file can't be found.
|
||||
RuntimeError: If the underlying calculator graph can't be successfully
|
||||
initialized or started.
|
||||
ValueError: If any of the following:
|
||||
a) If not exactly one of 'binary_graph_path' or 'graph_config' arguments
|
||||
is provided.
|
||||
b) If the graph validation process contains error.
|
||||
c) If the registered type name of the streams and side packets can't be
|
||||
found.
|
||||
d) If the calculator options of the calculator listed in
|
||||
calculator_params is not allowed to be modified.
|
||||
e) If the calculator options field is a repeated field but the field
|
||||
value to be set is not iterable.
|
||||
"""
|
||||
if bool(binary_graph_path) == bool(graph_config):
|
||||
raise ValueError(
|
||||
"Must provide exactly one of 'binary_graph_path' or 'graph_config'.")
|
||||
# MediaPipe package root path
|
||||
root_path = os.sep.join( os.path.abspath(__file__).split(os.sep)[:-3])
|
||||
resource_util.set_resource_dir(root_path)
|
||||
validated_graph = validated_graph_config.ValidatedGraphConfig()
|
||||
if binary_graph_path:
|
||||
validated_graph.initialize(
|
||||
binary_graph_path=os.path.join(root_path, binary_graph_path))
|
||||
else:
|
||||
validated_graph.initialize(graph_config=graph_config)
|
||||
|
||||
canonical_graph_config_proto = self._initialize_graph_interface(
|
||||
validated_graph, side_inputs, outputs)
|
||||
if calculator_params:
|
||||
self._modify_calculator_options(canonical_graph_config_proto,
|
||||
calculator_params)
|
||||
self._graph = calculator_graph.CalculatorGraph(
|
||||
graph_config=canonical_graph_config_proto)
|
||||
self._simulated_timestamp = 0
|
||||
self._graph_outputs = {}
|
||||
|
||||
def callback(stream_name: str, output_packet: packet.Packet) -> None:
|
||||
self._graph_outputs[stream_name] = output_packet
|
||||
|
||||
for stream_name in self._output_stream_type_info.keys():
|
||||
self._graph.observe_output_stream(stream_name, callback)
|
||||
|
||||
input_side_packets = {
|
||||
name: self._make_packet(self._side_input_type_info[name], data)
|
||||
for name, data in (side_inputs or {}).items()
|
||||
}
|
||||
self._graph.start_run(input_side_packets)
|
||||
|
||||
# TODO: Use "inspect.Parameter" to fetch the input argument names and
|
||||
# types from "_input_stream_type_info" and then auto generate the process
|
||||
# method signature by "inspect.Signature" in __init__.
|
||||
def process(
|
||||
self, input_data: Union[np.ndarray, Mapping[str,
|
||||
np.ndarray]]) -> NamedTuple:
|
||||
"""Processes a set of RGB image data and output SolutionOutputs.
|
||||
|
||||
Args:
|
||||
input_data: Either a single numpy ndarray object representing the solo
|
||||
image input of a graph or a mapping from the stream name to the image
|
||||
data that represents every input streams of a graph.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If input_data contains non image data.
|
||||
RuntimeError: If the underlying graph occurs any error.
|
||||
ValueError: If the input image data is not three channel RGB.
|
||||
|
||||
Returns:
|
||||
A NamedTuple object that contains the output data of a graph run.
|
||||
The field names in the NamedTuple object are mapping to the graph output
|
||||
stream names.
|
||||
|
||||
Examples:
|
||||
solution = solution_base.SolutionBase(graph_config=hand_landmark_graph)
|
||||
results = solution.process(cv2.imread('/tmp/hand0.png')[:, :, ::-1])
|
||||
print(results.detection)
|
||||
results = solution.process(
|
||||
{'video_in' : cv2.imread('/tmp/hand1.png')[:, :, ::-1]})
|
||||
print(results.hand_landmarks)
|
||||
"""
|
||||
self._graph_outputs.clear()
|
||||
|
||||
if isinstance(input_data, np.ndarray):
|
||||
if len(self._input_stream_type_info.keys()) != 1:
|
||||
raise ValueError(
|
||||
"Can't process single image input since the graph has more than one input streams."
|
||||
)
|
||||
input_dict = {next(iter(self._input_stream_type_info)): input_data}
|
||||
else:
|
||||
input_dict = input_data
|
||||
|
||||
# Set the timestamp increment to 33333 us to simulate the 30 fps video
|
||||
# input.
|
||||
self._simulated_timestamp += 33333
|
||||
for stream_name, data in input_dict.items():
|
||||
if self._input_stream_type_info[stream_name] == _PacketDataType.IMAGE:
|
||||
if data.shape[2] != RGB_CHANNELS:
|
||||
raise ValueError('Input image must contain three channel rgb data.')
|
||||
self._graph.add_packet_to_input_stream(
|
||||
stream=stream_name,
|
||||
packet=self._make_packet(_PacketDataType.IMAGE,
|
||||
data).at(self._simulated_timestamp))
|
||||
else:
|
||||
# TODO: Support audio data.
|
||||
raise NotImplementedError(
|
||||
f'SolutionBase can only process image data. '
|
||||
f'{self._input_stream_type_info[stream_name].name} '
|
||||
f'type is not supported yet.')
|
||||
|
||||
self._graph.wait_until_idle()
|
||||
# Create a NamedTuple object where the field names are mapping to the graph
|
||||
# output stream names.
|
||||
solution_outputs = collections.namedtuple(
|
||||
'SolutionOutputs', self._output_stream_type_info.keys())
|
||||
for stream_name in self._output_stream_type_info.keys():
|
||||
if stream_name in self._graph_outputs:
|
||||
setattr(
|
||||
solution_outputs, stream_name,
|
||||
self._get_packet_content(self._output_stream_type_info[stream_name],
|
||||
self._graph_outputs[stream_name]))
|
||||
else:
|
||||
setattr(solution_outputs, stream_name, None)
|
||||
|
||||
return solution_outputs
|
||||
|
||||
def close(self) -> None:
|
||||
"""Closes all the input sources and the graph."""
|
||||
self._graph.close()
|
||||
self._graph = None
|
||||
self._input_stream_type_info = None
|
||||
self._output_stream_type_info = None
|
||||
|
||||
def _initialize_graph_interface(
|
||||
self,
|
||||
validated_graph: validated_graph_config.ValidatedGraphConfig,
|
||||
side_inputs: Optional[Mapping[str, Any]] = None,
|
||||
outputs: Optional[List[str]] = None):
|
||||
"""Gets graph interface type information and returns the canonical graph config proto."""
|
||||
|
||||
canonical_graph_config_proto = calculator_pb2.CalculatorGraphConfig()
|
||||
canonical_graph_config_proto.ParseFromString(validated_graph.binary_config)
|
||||
|
||||
# Gets name from a 'TAG:index:name' str.
|
||||
def get_name(tag_index_name):
|
||||
return tag_index_name.split(':')[-1]
|
||||
|
||||
# Gets the packet type information of the input streams and output streams
|
||||
# from the validated calculator graph. The mappings from the stream names to
|
||||
# the packet data types is for deciding which packet creator and getter
|
||||
# methods to call in the process() method.
|
||||
def get_stream_packet_type(packet_tag_index_name):
|
||||
return _PacketDataType.from_registered_name(
|
||||
validated_graph.registered_stream_type_name(
|
||||
get_name(packet_tag_index_name)))
|
||||
|
||||
self._input_stream_type_info = {
|
||||
get_name(tag_index_name): get_stream_packet_type(tag_index_name)
|
||||
for tag_index_name in canonical_graph_config_proto.input_stream
|
||||
}
|
||||
|
||||
if not outputs:
|
||||
output_streams = canonical_graph_config_proto.output_stream
|
||||
else:
|
||||
output_streams = outputs
|
||||
self._output_stream_type_info = {
|
||||
get_name(tag_index_name): get_stream_packet_type(tag_index_name)
|
||||
for tag_index_name in output_streams
|
||||
}
|
||||
|
||||
# Gets the packet type information of the input side packets from the
|
||||
# validated calculator graph. The mappings from the side packet names to the
|
||||
# packet data types is for making the input_side_packets dict for graph
|
||||
# start_run().
|
||||
def get_side_packet_type(packet_tag_index_name):
|
||||
return _PacketDataType.from_registered_name(
|
||||
validated_graph.registered_side_packet_type_name(
|
||||
get_name(packet_tag_index_name)))
|
||||
|
||||
self._side_input_type_info = {
|
||||
get_name(tag_index_name): get_side_packet_type(tag_index_name)
|
||||
for tag_index_name, _ in (side_inputs or {}).items()
|
||||
}
|
||||
return canonical_graph_config_proto
|
||||
|
||||
def _modify_calculator_options(
|
||||
self, calculator_graph_config: calculator_pb2.CalculatorGraphConfig,
|
||||
calculator_params: Mapping[str, Any]) -> None:
|
||||
"""Modifies the CalculatorOptions of the calculators listed in calculator_params."""
|
||||
|
||||
# Reorganizes the calculator options field data by calculator name and puts
|
||||
# all the field data of the same calculator in a list.
|
||||
def generate_nested_calculator_params(flat_map):
|
||||
nested_map = {}
|
||||
for compound_name, field_value in flat_map.items():
|
||||
calculator_and_field_name = compound_name.split('.')
|
||||
if len(calculator_and_field_name) != 2:
|
||||
raise ValueError(
|
||||
f'The key "{compound_name}" in the calculator_params is invalid.')
|
||||
calculator_name = calculator_and_field_name[0]
|
||||
field_name = calculator_and_field_name[1]
|
||||
if calculator_name in nested_map:
|
||||
nested_map[calculator_name].append((field_name, field_value))
|
||||
else:
|
||||
nested_map[calculator_name] = [(field_name, field_value)]
|
||||
return nested_map
|
||||
|
||||
def modify_options_fields(calculator_options, options_field_list):
|
||||
for field_name, field_value in options_field_list:
|
||||
if field_value is None:
|
||||
calculator_options.ClearField(field_name)
|
||||
else:
|
||||
field_label = calculator_options.DESCRIPTOR.fields_by_name[
|
||||
field_name].label
|
||||
if field_label is descriptor.FieldDescriptor.LABEL_REPEATED:
|
||||
if not isinstance(field_value, Iterable):
|
||||
raise ValueError(
|
||||
f'{field_name} is a repeated proto field but the value '
|
||||
f'to be set is {type(field_value)}, which is not iterable.')
|
||||
# TODO: Support resetting the entire repeated field
|
||||
# (array-option) and changing the individual values in the repeated
|
||||
# field (array-element-option).
|
||||
calculator_options.ClearField(field_name)
|
||||
for elem in field_value:
|
||||
getattr(calculator_options, field_name).append(elem)
|
||||
else:
|
||||
setattr(calculator_options, field_name, field_value)
|
||||
|
||||
nested_calculator_params = generate_nested_calculator_params(
|
||||
calculator_params)
|
||||
|
||||
num_modified = 0
|
||||
for node in calculator_graph_config.node:
|
||||
if node.name not in nested_calculator_params:
|
||||
continue
|
||||
options_type = CALCULATOR_TO_OPTIONS.get(node.calculator)
|
||||
if options_type is None:
|
||||
raise ValueError(
|
||||
f'Modifying the calculator options of {node.name} is not supported.'
|
||||
)
|
||||
options_field_list = nested_calculator_params[node.name]
|
||||
if node.HasField('options') and node.node_options:
|
||||
raise ValueError(
|
||||
f'Cannot modify the calculator options of {node.name} because it '
|
||||
f'has both options and node_options fields.')
|
||||
if node.node_options:
|
||||
# The "node_options" case for the proto3 syntax.
|
||||
node_options_modified = False
|
||||
for elem in node.node_options:
|
||||
type_name = elem.type_url.split('/')[-1]
|
||||
if type_name == options_type.DESCRIPTOR.full_name:
|
||||
calculator_options = options_type.FromString(elem.value)
|
||||
modify_options_fields(calculator_options, options_field_list)
|
||||
elem.value = calculator_options.SerializeToString()
|
||||
node_options_modified = True
|
||||
break
|
||||
# There is no existing node_options being modified. Add a new
|
||||
# node_options instead.
|
||||
if not node_options_modified:
|
||||
calculator_options = options_type()
|
||||
modify_options_fields(calculator_options, options_field_list)
|
||||
node.node_options.add().Pack(calculator_options)
|
||||
else:
|
||||
# The "options" case for the proto2 syntax as well as the fallback
|
||||
# when the calculator doesn't have either "options" or "node_options".
|
||||
modify_options_fields(node.options.Extensions[options_type.ext],
|
||||
options_field_list)
|
||||
|
||||
num_modified += 1
|
||||
# Exits the loop early when every elements in nested_calculator_params
|
||||
# have been visited.
|
||||
if num_modified == len(nested_calculator_params):
|
||||
break
|
||||
|
||||
def _make_packet(self, packet_data_type: _PacketDataType,
|
||||
data: Any) -> packet.Packet:
|
||||
if packet_data_type == _PacketDataType.IMAGE:
|
||||
return packet_creator.create_image_frame(
|
||||
data, image_format=image_frame.ImageFormat.SRGB)
|
||||
else:
|
||||
return getattr(packet_creator, 'create_' + packet_data_type.value)(data)
|
||||
|
||||
def _get_packet_content(self, packet_data_type: _PacketDataType,
|
||||
output_packet: packet.Packet) -> Any:
|
||||
if packet_data_type == _PacketDataType.STRING:
|
||||
return packet_getter.get_str(output_packet)
|
||||
elif packet_data_type == _PacketDataType.IMAGE:
|
||||
return packet_getter.get_image_frame(output_packet).numpy_view()
|
||||
else:
|
||||
return getattr(packet_getter, 'get_' + packet_data_type.value)(
|
||||
output_packet)
|
||||
@@ -0,0 +1,288 @@
|
||||
# Copyright 2020 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.
|
||||
|
||||
# Lint as: python3
|
||||
"""Tests for mediapipe.python.solution_base."""
|
||||
|
||||
from absl.testing import absltest
|
||||
from absl.testing import parameterized
|
||||
import numpy as np
|
||||
|
||||
from google.protobuf import text_format
|
||||
from mediapipe.framework import calculator_pb2
|
||||
from mediapipe.framework.formats import detection_pb2
|
||||
from mediapipe.python import solution_base
|
||||
|
||||
CALCULATOR_OPTIONS_TEST_GRAPH_CONFIG = """
|
||||
input_stream: 'image_in'
|
||||
output_stream: 'image_out'
|
||||
node {
|
||||
name: 'ImageTransformation'
|
||||
calculator: 'ImageTransformationCalculator'
|
||||
input_stream: 'IMAGE:image_in'
|
||||
output_stream: 'IMAGE:image_out'
|
||||
options: {
|
||||
[mediapipe.ImageTransformationCalculatorOptions.ext] {
|
||||
output_width: 10
|
||||
output_height: 10
|
||||
}
|
||||
}
|
||||
node_options: {
|
||||
[type.googleapis.com/mediapipe.ImageTransformationCalculatorOptions] {
|
||||
output_width: 10
|
||||
output_height: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class SolutionBaseTest(parameterized.TestCase):
|
||||
|
||||
def test_invalid_initialization_arguments(self):
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
'Must provide exactly one of \'binary_graph_path\' or \'graph_config\'.'
|
||||
):
|
||||
solution_base.SolutionBase()
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
'Must provide exactly one of \'binary_graph_path\' or \'graph_config\'.'
|
||||
):
|
||||
solution_base.SolutionBase(
|
||||
graph_config=calculator_pb2.CalculatorGraphConfig(),
|
||||
binary_graph_path='/tmp/no_such.binarypb')
|
||||
|
||||
@parameterized.named_parameters(('no_graph_input_output_stream', """
|
||||
node {
|
||||
calculator: 'PassThroughCalculator'
|
||||
input_stream: 'in'
|
||||
output_stream: 'out'
|
||||
}
|
||||
""", RuntimeError, 'does not have a corresponding output stream.'),
|
||||
('calcualtor_io_mismatch', """
|
||||
node {
|
||||
calculator: 'PassThroughCalculator'
|
||||
input_stream: 'in'
|
||||
input_stream: 'in2'
|
||||
output_stream: 'out'
|
||||
}
|
||||
""", ValueError, 'must use matching tags and indexes.'),
|
||||
('unkown_registered_stream_type_name', """
|
||||
input_stream: 'in'
|
||||
output_stream: 'out'
|
||||
node {
|
||||
calculator: 'PassThroughCalculator'
|
||||
input_stream: 'in'
|
||||
output_stream: 'out'
|
||||
}
|
||||
""", RuntimeError, 'Unable to find the type for stream \"in\".'))
|
||||
def test_invalid_config(self, text_config, error_type, error_message):
|
||||
config_proto = text_format.Parse(text_config,
|
||||
calculator_pb2.CalculatorGraphConfig())
|
||||
with self.assertRaisesRegex(error_type, error_message):
|
||||
solution_base.SolutionBase(graph_config=config_proto)
|
||||
|
||||
def test_invalid_input_data_type(self):
|
||||
text_config = """
|
||||
input_stream: 'input_detections'
|
||||
output_stream: 'output_detections'
|
||||
node {
|
||||
calculator: 'DetectionUniqueIdCalculator'
|
||||
input_stream: 'DETECTIONS:input_detections'
|
||||
output_stream: 'DETECTIONS:output_detections'
|
||||
}
|
||||
"""
|
||||
config_proto = text_format.Parse(text_config,
|
||||
calculator_pb2.CalculatorGraphConfig())
|
||||
solution = solution_base.SolutionBase(graph_config=config_proto)
|
||||
detection = detection_pb2.Detection()
|
||||
text_format.Parse('score: 0.5', detection)
|
||||
with self.assertRaisesRegex(
|
||||
NotImplementedError,
|
||||
'SolutionBase can only process image data. PROTO_LIST type is not supported.'
|
||||
):
|
||||
solution.process({'input_detections': detection})
|
||||
|
||||
def test_invalid_input_image_data(self):
|
||||
text_config = """
|
||||
input_stream: 'image_in'
|
||||
output_stream: 'image_out'
|
||||
node {
|
||||
calculator: 'ImageTransformationCalculator'
|
||||
input_stream: 'IMAGE:image_in'
|
||||
output_stream: 'IMAGE:transformed_image_in'
|
||||
}
|
||||
node {
|
||||
calculator: 'ImageTransformationCalculator'
|
||||
input_stream: 'IMAGE:transformed_image_in'
|
||||
output_stream: 'IMAGE:image_out'
|
||||
}
|
||||
"""
|
||||
config_proto = text_format.Parse(text_config,
|
||||
calculator_pb2.CalculatorGraphConfig())
|
||||
solution = solution_base.SolutionBase(graph_config=config_proto)
|
||||
with self.assertRaisesRegex(
|
||||
ValueError, 'Input image must contain three channel rgb data.'):
|
||||
solution.process(np.arange(36, dtype=np.uint8).reshape(3, 3, 4))
|
||||
|
||||
@parameterized.named_parameters(('graph_without_side_packets', """
|
||||
input_stream: 'image_in'
|
||||
output_stream: 'image_out'
|
||||
node {
|
||||
calculator: 'ImageTransformationCalculator'
|
||||
input_stream: 'IMAGE:image_in'
|
||||
output_stream: 'IMAGE:transformed_image_in'
|
||||
}
|
||||
node {
|
||||
calculator: 'ImageTransformationCalculator'
|
||||
input_stream: 'IMAGE:transformed_image_in'
|
||||
output_stream: 'IMAGE:image_out'
|
||||
}
|
||||
""", None), ('graph_with_side_packets', """
|
||||
input_stream: 'image_in'
|
||||
input_side_packet: 'allow_signal'
|
||||
input_side_packet: 'rotation_degrees'
|
||||
output_stream: 'image_out'
|
||||
node {
|
||||
calculator: 'ImageTransformationCalculator'
|
||||
input_stream: 'IMAGE:image_in'
|
||||
input_side_packet: 'ROTATION_DEGREES:rotation_degrees'
|
||||
output_stream: 'IMAGE:transformed_image_in'
|
||||
}
|
||||
node {
|
||||
calculator: 'GateCalculator'
|
||||
input_stream: 'transformed_image_in'
|
||||
input_side_packet: 'ALLOW:allow_signal'
|
||||
output_stream: 'image_out_to_transform'
|
||||
}
|
||||
node {
|
||||
calculator: 'ImageTransformationCalculator'
|
||||
input_stream: 'IMAGE:image_out_to_transform'
|
||||
input_side_packet: 'ROTATION_DEGREES:rotation_degrees'
|
||||
output_stream: 'IMAGE:image_out'
|
||||
}""", {
|
||||
'allow_signal': True,
|
||||
'rotation_degrees': 0
|
||||
}))
|
||||
def test_solution_process(self, text_config, side_inputs):
|
||||
self._process_and_verify(
|
||||
config_proto=text_format.Parse(text_config,
|
||||
calculator_pb2.CalculatorGraphConfig()),
|
||||
side_inputs=side_inputs)
|
||||
|
||||
def test_invalid_calculator_options(self):
|
||||
text_config = """
|
||||
input_stream: 'image_in'
|
||||
output_stream: 'image_out'
|
||||
node {
|
||||
calculator: 'ImageTransformationCalculator'
|
||||
input_stream: 'IMAGE:image_in'
|
||||
output_stream: 'IMAGE:transformed_image_in'
|
||||
}
|
||||
node {
|
||||
name: 'SignalGate'
|
||||
calculator: 'GateCalculator'
|
||||
input_stream: 'transformed_image_in'
|
||||
input_side_packet: 'ALLOW:allow_signal'
|
||||
output_stream: 'image_out_to_transform'
|
||||
}
|
||||
node {
|
||||
calculator: 'ImageTransformationCalculator'
|
||||
input_stream: 'IMAGE:image_out_to_transform'
|
||||
output_stream: 'IMAGE:image_out'
|
||||
}
|
||||
"""
|
||||
config_proto = text_format.Parse(text_config,
|
||||
calculator_pb2.CalculatorGraphConfig())
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
'Modifying the calculator options of SignalGate is not supported.'):
|
||||
solution_base.SolutionBase(
|
||||
graph_config=config_proto,
|
||||
calculator_params={'SignalGate.invalid_field': 'I am invalid'})
|
||||
|
||||
def test_calculator_has_both_options_and_node_options(self):
|
||||
config_proto = text_format.Parse(CALCULATOR_OPTIONS_TEST_GRAPH_CONFIG,
|
||||
calculator_pb2.CalculatorGraphConfig())
|
||||
with self.assertRaisesRegex(ValueError,
|
||||
'has both options and node_options fields.'):
|
||||
solution_base.SolutionBase(
|
||||
graph_config=config_proto,
|
||||
calculator_params={
|
||||
'ImageTransformation.output_width': 0,
|
||||
'ImageTransformation.output_height': 0
|
||||
})
|
||||
|
||||
def test_modifying_calculator_proto2_options(self):
|
||||
config_proto = text_format.Parse(CALCULATOR_OPTIONS_TEST_GRAPH_CONFIG,
|
||||
calculator_pb2.CalculatorGraphConfig())
|
||||
# To test proto2 options only, remove the proto3 node_options field from the
|
||||
# graph config.
|
||||
self.assertEqual('ImageTransformation', config_proto.node[0].name)
|
||||
config_proto.node[0].ClearField('node_options')
|
||||
self._process_and_verify(
|
||||
config_proto=config_proto,
|
||||
calculator_params={
|
||||
'ImageTransformation.output_width': 0,
|
||||
'ImageTransformation.output_height': 0
|
||||
})
|
||||
|
||||
def test_modifying_calculator_proto3_node_options(self):
|
||||
config_proto = text_format.Parse(CALCULATOR_OPTIONS_TEST_GRAPH_CONFIG,
|
||||
calculator_pb2.CalculatorGraphConfig())
|
||||
# To test proto3 node options only, remove the proto2 options field from the
|
||||
# graph config.
|
||||
self.assertEqual('ImageTransformation', config_proto.node[0].name)
|
||||
config_proto.node[0].ClearField('options')
|
||||
self._process_and_verify(
|
||||
config_proto=config_proto,
|
||||
calculator_params={
|
||||
'ImageTransformation.output_width': 0,
|
||||
'ImageTransformation.output_height': 0
|
||||
})
|
||||
|
||||
def test_adding_calculator_options(self):
|
||||
config_proto = text_format.Parse(CALCULATOR_OPTIONS_TEST_GRAPH_CONFIG,
|
||||
calculator_pb2.CalculatorGraphConfig())
|
||||
# To test a calculator with no options field, remove both proto2 options and
|
||||
# proto3 node_options fields from the graph config.
|
||||
self.assertEqual('ImageTransformation', config_proto.node[0].name)
|
||||
config_proto.node[0].ClearField('options')
|
||||
config_proto.node[0].ClearField('node_options')
|
||||
self._process_and_verify(
|
||||
config_proto=config_proto,
|
||||
calculator_params={
|
||||
'ImageTransformation.output_width': 0,
|
||||
'ImageTransformation.output_height': 0
|
||||
})
|
||||
|
||||
def _process_and_verify(self,
|
||||
config_proto,
|
||||
side_inputs=None,
|
||||
calculator_params=None):
|
||||
input_image = np.arange(27, dtype=np.uint8).reshape(3, 3, 3)
|
||||
solution = solution_base.SolutionBase(
|
||||
graph_config=config_proto,
|
||||
side_inputs=side_inputs,
|
||||
calculator_params=calculator_params)
|
||||
outputs = solution.process(input_image)
|
||||
self.assertTrue(np.array_equal(input_image, outputs.image_out))
|
||||
outputs2 = solution.process({'image_in': input_image})
|
||||
self.assertTrue(np.array_equal(input_image, outputs2.image_out))
|
||||
solution.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
absltest.main()
|
||||
@@ -0,0 +1,20 @@
|
||||
# Copyright 2020 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.
|
||||
|
||||
"""MediaPipe Solutions Python API."""
|
||||
|
||||
import mediapipe.python.solutions.drawing_utils
|
||||
import mediapipe.python.solutions.face_mesh
|
||||
import mediapipe.python.solutions.hands
|
||||
import mediapipe.python.solutions.pose
|
||||
@@ -0,0 +1,114 @@
|
||||
# Copyright 2020 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.
|
||||
|
||||
# Lint as: python3
|
||||
"""MediaPipe solution drawing utils."""
|
||||
|
||||
import math
|
||||
from typing import List, Tuple, Union
|
||||
|
||||
import cv2
|
||||
import dataclasses
|
||||
import numpy as np
|
||||
|
||||
from mediapipe.framework.formats import landmark_pb2
|
||||
|
||||
RGB_CHANNELS = 3
|
||||
RED_COLOR = (0, 0, 255)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class DrawingSpec:
|
||||
# Color for drawing the annotation. Default to the green color.
|
||||
color: Tuple[int, int, int] = (0, 255, 0)
|
||||
# Thickness for drawing the annotation. Default to 2 pixels.
|
||||
thickness: int = 2
|
||||
# Circle radius. Default to 2 pixels.
|
||||
circle_radius: int = 2
|
||||
|
||||
|
||||
def _normalized_to_pixel_coordinates(
|
||||
normalized_x: float, normalized_y: float, image_width: int,
|
||||
image_height: int) -> Union[None, Tuple[int, int]]:
|
||||
"""Converts normalized value pair to pixel coordinates."""
|
||||
|
||||
# Checks if the float value is between 0 and 1.
|
||||
def is_valid_normalized_value(value: float) -> bool:
|
||||
return (value > 0 or math.isclose(0, value)) and (value < 1 or
|
||||
math.isclose(1, value))
|
||||
|
||||
if not (is_valid_normalized_value(normalized_x) and
|
||||
is_valid_normalized_value(normalized_y)):
|
||||
# TODO: Draw coordinates even if it's outside of the image bounds.
|
||||
return None
|
||||
x_px = min(math.floor(normalized_x * image_width), image_width - 1)
|
||||
y_px = min(math.floor(normalized_y * image_height), image_height - 1)
|
||||
return x_px, y_px
|
||||
|
||||
|
||||
def draw_landmarks(
|
||||
image: np.ndarray,
|
||||
landmark_list: landmark_pb2.NormalizedLandmarkList,
|
||||
connections: List[Tuple[int, int]] = None,
|
||||
landmark_drawing_spec: DrawingSpec = DrawingSpec(color=RED_COLOR),
|
||||
connection_drawing_spec: DrawingSpec = DrawingSpec()):
|
||||
"""Draws the landmarks and the connections on the image.
|
||||
|
||||
Args:
|
||||
image: A three channel RGB image represented as numpy ndarray.
|
||||
landmark_list: A normalized landmark list proto message to be annotated on
|
||||
the image.
|
||||
connections: A list of landmark index tuples that specifies how landmarks to
|
||||
be connected in the drawing.
|
||||
landmark_drawing_spec: A DrawingSpec object that specifies the landmarks'
|
||||
drawing settings such as color, line thickness, and circle radius.
|
||||
connection_drawing_spec: A DrawingSpec object that specifies the
|
||||
connections' drawing settings such as color and line thickness.
|
||||
|
||||
Raises:
|
||||
ValueError: If one of the followings:
|
||||
a) If the input image is not three channel RGB.
|
||||
b) If any connetions contain invalid landmark index.
|
||||
"""
|
||||
if not landmark_list:
|
||||
return
|
||||
if image.shape[2] != RGB_CHANNELS:
|
||||
raise ValueError('Input image must contain three channel rgb data.')
|
||||
image_rows, image_cols, _ = image.shape
|
||||
idx_to_coordinates = {}
|
||||
for idx, landmark in enumerate(landmark_list.landmark):
|
||||
if landmark.visibility < 0 or landmark.presence < 0:
|
||||
continue
|
||||
landmark_px = _normalized_to_pixel_coordinates(landmark.x, landmark.y,
|
||||
image_cols, image_rows)
|
||||
if landmark_px:
|
||||
idx_to_coordinates[idx] = landmark_px
|
||||
if connections:
|
||||
num_landmarks = len(landmark_list.landmark)
|
||||
# Draws the connections if the start and end landmarks are both visible.
|
||||
for connection in connections:
|
||||
start_idx = connection[0]
|
||||
end_idx = connection[1]
|
||||
if not (0 <= start_idx < num_landmarks and 0 <= end_idx < num_landmarks):
|
||||
raise ValueError(f'Landmark index is out of range. Invalid connection '
|
||||
f'from landmark #{start_idx} to landmark #{end_idx}.')
|
||||
if start_idx in idx_to_coordinates and end_idx in idx_to_coordinates:
|
||||
cv2.line(image, idx_to_coordinates[start_idx],
|
||||
idx_to_coordinates[end_idx], connection_drawing_spec.color,
|
||||
connection_drawing_spec.thickness)
|
||||
# Draws landmark points after finishing the connection lines, which is
|
||||
# aesthetically better.
|
||||
for landmark_px in idx_to_coordinates.values():
|
||||
cv2.circle(image, landmark_px, landmark_drawing_spec.circle_radius,
|
||||
landmark_drawing_spec.color, landmark_drawing_spec.thickness)
|
||||
@@ -0,0 +1,144 @@
|
||||
# Copyright 2020 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.
|
||||
|
||||
# Lint as: python3
|
||||
"""Tests for mediapipe.python.solutions.drawing_utils."""
|
||||
|
||||
from absl.testing import absltest
|
||||
from absl.testing import parameterized
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from google.protobuf import text_format
|
||||
|
||||
from mediapipe.framework.formats import landmark_pb2
|
||||
from mediapipe.python.solutions import drawing_utils
|
||||
|
||||
DEFAULT_CONNECTION_DRAWING_SPEC = drawing_utils.DrawingSpec()
|
||||
DEFAULT_LANDMARK_DRAWING_SPEC = drawing_utils.DrawingSpec(color=(0, 0, 255))
|
||||
|
||||
|
||||
class DrawingUtilTest(parameterized.TestCase):
|
||||
|
||||
def test_invalid_input_image(self):
|
||||
image = np.arange(18, dtype=np.uint8).reshape(3, 3, 2)
|
||||
with self.assertRaisesRegex(
|
||||
ValueError, 'Input image must contain three channel rgb data.'):
|
||||
drawing_utils.draw_landmarks(image, landmark_pb2.NormalizedLandmarkList())
|
||||
|
||||
def test_invalid_connection(self):
|
||||
landmark_list = text_format.Parse(
|
||||
'landmark {x: 0.5 y: 0.5} landmark {x: 0.2 y: 0.2}',
|
||||
landmark_pb2.NormalizedLandmarkList())
|
||||
image = np.arange(27, dtype=np.uint8).reshape(3, 3, 3)
|
||||
with self.assertRaisesRegex(ValueError, 'Landmark index is out of range.'):
|
||||
drawing_utils.draw_landmarks(image, landmark_list, [(0, 2)])
|
||||
|
||||
@parameterized.named_parameters(
|
||||
('landmark_list_has_only_one_element', 'landmark {x: 0.1 y: 0.1}'),
|
||||
('second_landmark_is_invisible',
|
||||
'landmark {x: 0.1 y: 0.1} landmark {x: 0.5 y: 0.5 visibility: -1.0}'))
|
||||
def test_draw_single_landmark_point(self, landmark_list_text):
|
||||
landmark_list = text_format.Parse(landmark_list_text,
|
||||
landmark_pb2.NormalizedLandmarkList())
|
||||
image = np.zeros((100, 100, 3), np.uint8)
|
||||
expected_result = np.copy(image)
|
||||
cv2.circle(expected_result, (10, 10),
|
||||
DEFAULT_LANDMARK_DRAWING_SPEC.circle_radius,
|
||||
DEFAULT_LANDMARK_DRAWING_SPEC.color,
|
||||
DEFAULT_LANDMARK_DRAWING_SPEC.thickness)
|
||||
drawing_utils.draw_landmarks(image, landmark_list)
|
||||
np.testing.assert_array_equal(image, expected_result)
|
||||
|
||||
@parameterized.named_parameters(
|
||||
('landmarks_have_x_and_y_only',
|
||||
'landmark {x: 0.1 y: 0.5} landmark {x: 0.5 y: 0.1}'),
|
||||
('landmark_zero_visibility_and_presence',
|
||||
'landmark {x: 0.1 y: 0.5 presence: 0.0}'
|
||||
'landmark {x: 0.5 y: 0.1 visibility: 0.0}'))
|
||||
def test_draw_landmarks_and_connections(self, landmark_list_text):
|
||||
landmark_list = text_format.Parse(landmark_list_text,
|
||||
landmark_pb2.NormalizedLandmarkList())
|
||||
image = np.zeros((100, 100, 3), np.uint8)
|
||||
expected_result = np.copy(image)
|
||||
start_point = (10, 50)
|
||||
end_point = (50, 10)
|
||||
cv2.line(expected_result, start_point, end_point,
|
||||
DEFAULT_CONNECTION_DRAWING_SPEC.color,
|
||||
DEFAULT_CONNECTION_DRAWING_SPEC.thickness)
|
||||
cv2.circle(expected_result, start_point,
|
||||
DEFAULT_LANDMARK_DRAWING_SPEC.circle_radius,
|
||||
DEFAULT_LANDMARK_DRAWING_SPEC.color,
|
||||
DEFAULT_LANDMARK_DRAWING_SPEC.thickness)
|
||||
cv2.circle(expected_result, end_point,
|
||||
DEFAULT_LANDMARK_DRAWING_SPEC.circle_radius,
|
||||
DEFAULT_LANDMARK_DRAWING_SPEC.color,
|
||||
DEFAULT_LANDMARK_DRAWING_SPEC.thickness)
|
||||
drawing_utils.draw_landmarks(
|
||||
image=image, landmark_list=landmark_list, connections=[(0, 1)])
|
||||
np.testing.assert_array_equal(image, expected_result)
|
||||
|
||||
def test_min_and_max_coordinate_values(self):
|
||||
landmark_list = text_format.Parse(
|
||||
'landmark {x: 0.0 y: 1.0}'
|
||||
'landmark {x: 1.0 y: 0.0}', landmark_pb2.NormalizedLandmarkList())
|
||||
image = np.zeros((100, 100, 3), np.uint8)
|
||||
expected_result = np.copy(image)
|
||||
start_point = (0, 99)
|
||||
end_point = (99, 0)
|
||||
cv2.line(expected_result, start_point, end_point,
|
||||
DEFAULT_CONNECTION_DRAWING_SPEC.color,
|
||||
DEFAULT_CONNECTION_DRAWING_SPEC.thickness)
|
||||
cv2.circle(expected_result, start_point,
|
||||
DEFAULT_LANDMARK_DRAWING_SPEC.circle_radius,
|
||||
DEFAULT_LANDMARK_DRAWING_SPEC.color,
|
||||
DEFAULT_LANDMARK_DRAWING_SPEC.thickness)
|
||||
cv2.circle(expected_result, end_point,
|
||||
DEFAULT_LANDMARK_DRAWING_SPEC.circle_radius,
|
||||
DEFAULT_LANDMARK_DRAWING_SPEC.color,
|
||||
DEFAULT_LANDMARK_DRAWING_SPEC.thickness)
|
||||
drawing_utils.draw_landmarks(
|
||||
image=image, landmark_list=landmark_list, connections=[(0, 1)])
|
||||
np.testing.assert_array_equal(image, expected_result)
|
||||
|
||||
def test_drawing_spec(self):
|
||||
landmark_list = text_format.Parse(
|
||||
'landmark {x: 0.1 y: 0.1}'
|
||||
'landmark {x: 0.8 y: 0.8}', landmark_pb2.NormalizedLandmarkList())
|
||||
image = np.zeros((100, 100, 3), np.uint8)
|
||||
landmark_drawing_spec = drawing_utils.DrawingSpec(
|
||||
color=(0, 0, 255), thickness=5)
|
||||
connection_drawing_spec = drawing_utils.DrawingSpec(
|
||||
color=(255, 0, 0), thickness=3)
|
||||
expected_result = np.copy(image)
|
||||
start_point = (10, 10)
|
||||
end_point = (80, 80)
|
||||
cv2.line(expected_result, start_point, end_point,
|
||||
connection_drawing_spec.color, connection_drawing_spec.thickness)
|
||||
cv2.circle(expected_result, start_point,
|
||||
landmark_drawing_spec.circle_radius, landmark_drawing_spec.color,
|
||||
landmark_drawing_spec.thickness)
|
||||
cv2.circle(expected_result, end_point, landmark_drawing_spec.circle_radius,
|
||||
landmark_drawing_spec.color, landmark_drawing_spec.thickness)
|
||||
drawing_utils.draw_landmarks(
|
||||
image=image,
|
||||
landmark_list=landmark_list,
|
||||
connections=[(0, 1)],
|
||||
landmark_drawing_spec=landmark_drawing_spec,
|
||||
connection_drawing_spec=connection_drawing_spec)
|
||||
np.testing.assert_array_equal(image, expected_result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
absltest.main()
|
||||
@@ -0,0 +1,307 @@
|
||||
# Copyright 2020 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.
|
||||
|
||||
# Lint as: python3
|
||||
"""MediaPipe FaceMesh."""
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
# pylint: disable=unused-import
|
||||
from mediapipe.calculators.core import gate_calculator_pb2
|
||||
from mediapipe.calculators.core import split_vector_calculator_pb2
|
||||
from mediapipe.calculators.tensor import inference_calculator_pb2
|
||||
from mediapipe.calculators.tensor import tensors_to_classification_calculator_pb2
|
||||
from mediapipe.calculators.tensor import tensors_to_detections_calculator_pb2
|
||||
from mediapipe.calculators.tensor import tensors_to_landmarks_calculator_pb2
|
||||
from mediapipe.calculators.tflite import ssd_anchors_calculator_pb2
|
||||
from mediapipe.calculators.util import association_calculator_pb2
|
||||
from mediapipe.calculators.util import detections_to_rects_calculator_pb2
|
||||
from mediapipe.calculators.util import logic_calculator_pb2
|
||||
from mediapipe.calculators.util import non_max_suppression_calculator_pb2
|
||||
from mediapipe.calculators.util import rect_transformation_calculator_pb2
|
||||
from mediapipe.calculators.util import thresholding_calculator_pb2
|
||||
# pylint: enable=unused-import
|
||||
from mediapipe.python.solution_base import SolutionBase
|
||||
|
||||
BINARYPB_FILE_PATH = 'mediapipe/modules/face_landmark/face_landmark_front_cpu.binarypb'
|
||||
FACE_CONNECTIONS = frozenset([
|
||||
# Lips.
|
||||
(61, 146),
|
||||
(146, 91),
|
||||
(91, 181),
|
||||
(181, 84),
|
||||
(84, 17),
|
||||
(17, 314),
|
||||
(314, 405),
|
||||
(405, 321),
|
||||
(321, 375),
|
||||
(375, 291),
|
||||
(61, 185),
|
||||
(185, 40),
|
||||
(40, 39),
|
||||
(39, 37),
|
||||
(37, 0),
|
||||
(0, 267),
|
||||
(267, 269),
|
||||
(269, 270),
|
||||
(270, 409),
|
||||
(409, 291),
|
||||
(78, 95),
|
||||
(95, 88),
|
||||
(88, 178),
|
||||
(178, 87),
|
||||
(87, 14),
|
||||
(14, 317),
|
||||
(317, 402),
|
||||
(402, 318),
|
||||
(318, 324),
|
||||
(324, 308),
|
||||
(78, 191),
|
||||
(191, 80),
|
||||
(80, 81),
|
||||
(81, 82),
|
||||
(82, 13),
|
||||
(13, 312),
|
||||
(312, 311),
|
||||
(311, 310),
|
||||
(310, 415),
|
||||
(415, 308),
|
||||
# Left eye.
|
||||
(33, 7),
|
||||
(7, 163),
|
||||
(163, 144),
|
||||
(144, 145),
|
||||
(145, 153),
|
||||
(153, 154),
|
||||
(154, 155),
|
||||
(155, 133),
|
||||
(33, 246),
|
||||
(246, 161),
|
||||
(161, 160),
|
||||
(160, 159),
|
||||
(159, 158),
|
||||
(158, 157),
|
||||
(157, 173),
|
||||
(173, 133),
|
||||
# Left eyebrow.
|
||||
(46, 53),
|
||||
(53, 52),
|
||||
(52, 65),
|
||||
(65, 55),
|
||||
(70, 63),
|
||||
(63, 105),
|
||||
(105, 66),
|
||||
(66, 107),
|
||||
# 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),
|
||||
(398, 362),
|
||||
# Right eyebrow.
|
||||
(276, 283),
|
||||
(283, 282),
|
||||
(282, 295),
|
||||
(295, 285),
|
||||
(300, 293),
|
||||
(293, 334),
|
||||
(334, 296),
|
||||
(296, 336),
|
||||
# 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),
|
||||
(378, 400),
|
||||
(400, 377),
|
||||
(377, 152),
|
||||
(152, 148),
|
||||
(148, 176),
|
||||
(176, 149),
|
||||
(149, 150),
|
||||
(150, 136),
|
||||
(136, 172),
|
||||
(172, 58),
|
||||
(58, 132),
|
||||
(132, 93),
|
||||
(93, 234),
|
||||
(234, 127),
|
||||
(127, 162),
|
||||
(162, 21),
|
||||
(21, 54),
|
||||
(54, 103),
|
||||
(103, 67),
|
||||
(67, 109),
|
||||
(109, 10)
|
||||
])
|
||||
|
||||
|
||||
class FaceMesh(SolutionBase):
|
||||
"""MediaPipe FaceMesh.
|
||||
|
||||
MediaPipe FaceMesh processes an RGB image and returns the face landmarks on
|
||||
each detected face.
|
||||
|
||||
Usage examples:
|
||||
import cv2
|
||||
import mediapipe as mp
|
||||
mp_drawing = mp.solutions.drawing_utils
|
||||
mp_face_mesh = mp.solutions.face_mesh
|
||||
|
||||
# For static images:
|
||||
face_mesh = mp_face_mesh.FaceMesh(
|
||||
static_image_mode=True,
|
||||
max_num_faces=1,
|
||||
min_detection_confidence=0.5)
|
||||
drawing_spec = mp_drawing.DrawingSpec(thickness=1, circle_radius=1)
|
||||
for idx, file in enumerate(file_list):
|
||||
image = cv2.imread(file)
|
||||
# Convert the BGR image to RGB before processing.
|
||||
results = face_mesh.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
||||
|
||||
# Print and draw face mesh landmarks on the image.
|
||||
if not results.multi_face_landmarks:
|
||||
continue
|
||||
annotated_image = image.copy()
|
||||
for face_landmarks in results.multi_face_landmarks:
|
||||
print('face_landmarks:', face_landmarks)
|
||||
mp_drawing.draw_landmarks(
|
||||
image=annotated_image,
|
||||
landmark_list=face_landmarks,
|
||||
connections=mp_face_mesh.FACE_CONNECTIONS,
|
||||
landmark_drawing_spec=drawing_spec,
|
||||
connection_drawing_spec=drawing_spec)
|
||||
cv2.imwrite('/tmp/annotated_image' + str(idx) + '.png', image)
|
||||
face_mesh.close()
|
||||
|
||||
# For webcam input:
|
||||
face_mesh = mp_face_mesh.FaceMesh(
|
||||
min_detection_confidence=0.5, min_tracking_confidence=0.5)
|
||||
drawing_spec = mp_drawing.DrawingSpec(thickness=1, circle_radius=1)
|
||||
cap = cv2.VideoCapture(0)
|
||||
while cap.isOpened():
|
||||
success, image = cap.read()
|
||||
if not success:
|
||||
break
|
||||
|
||||
# Flip the image horizontally for a later selfie-view display, and convert
|
||||
# the BGR image to RGB.
|
||||
image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB)
|
||||
# To improve performance, optionally mark the image as not writeable to
|
||||
# pass by reference.
|
||||
image.flags.writeable = False
|
||||
results = face_mesh.process(image)
|
||||
|
||||
# Draw the face mesh annotations on the image.
|
||||
image.flags.writeable = True
|
||||
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
||||
if results.multi_face_landmarks:
|
||||
for face_landmarks in results.multi_face_landmarks:
|
||||
mp_drawing.draw_landmarks(
|
||||
image=image,
|
||||
landmark_list=face_landmarks,
|
||||
connections=mp_face_mesh.FACE_CONNECTIONS,
|
||||
landmark_drawing_spec=drawing_spec,
|
||||
connection_drawing_spec=drawing_spec)
|
||||
cv2.imshow('MediaPipe FaceMesh', image)
|
||||
if cv2.waitKey(5) & 0xFF == 27:
|
||||
break
|
||||
face_mesh.close()
|
||||
cap.release()
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
static_image_mode=False,
|
||||
max_num_faces=2,
|
||||
min_detection_confidence=0.5,
|
||||
min_tracking_confidence=0.5):
|
||||
"""Initializes a MediaPipe FaceMesh object.
|
||||
|
||||
Args:
|
||||
static_image_mode: If set to False, the solution treats the input images
|
||||
as a video stream. It will try to detect faces in the first input
|
||||
images, and upon a successful detection further localizes the face
|
||||
landmarks. In subsequent images, once all "max_num_faces" faces are
|
||||
detected and the corresponding face landmarks are localized, it simply
|
||||
tracks those landmarks without invoking another detection until it loses
|
||||
track of any of the faces. This reduces latency and is ideal for
|
||||
processing video frames. If set to True, face detection runs on every
|
||||
input image, ideal for processing a batch of static, possibly unrelated,
|
||||
images. Default to False.
|
||||
max_num_faces: Maximum number of faces to detect. Default to 2.
|
||||
min_detection_confidence: Minimum confidence value ([0.0, 1.0]) from the
|
||||
face detection model for the detection to be considered successful.
|
||||
Default to 0.5.
|
||||
min_tracking_confidence: Minimum confidence value ([0.0, 1.0]) from the
|
||||
landmark-tracking model for the face landmarks to be considered tracked
|
||||
successfully, or otherwise face detection will be invoked automatically
|
||||
on the next input image. Setting it to a higher value can increase
|
||||
robustness of the solution, at the expense of a higher latency. Ignored
|
||||
if "static_image_mode" is True, where face detection simply runs on
|
||||
every image. Default to 0.5.
|
||||
"""
|
||||
super().__init__(
|
||||
binary_graph_path=BINARYPB_FILE_PATH,
|
||||
side_inputs={
|
||||
'num_faces': max_num_faces,
|
||||
'can_skip_detection': not static_image_mode,
|
||||
},
|
||||
calculator_params={
|
||||
'facedetectionfrontcpu__TensorsToDetectionsCalculator.min_score_thresh':
|
||||
min_detection_confidence,
|
||||
'facelandmarkcpu__ThresholdingCalculator.threshold':
|
||||
min_tracking_confidence,
|
||||
},
|
||||
outputs=['multi_face_landmarks'])
|
||||
|
||||
def process(self, image: np.ndarray) -> NamedTuple:
|
||||
"""Processes an RGB image and returns the face landmarks on each detected face.
|
||||
|
||||
Args:
|
||||
image: An RGB image represented as a numpy ndarray.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the underlying graph occurs any error.
|
||||
ValueError: If the input image is not three channel RGB.
|
||||
|
||||
Returns:
|
||||
A NamedTuple object with a "multi_face_landmarks" field that contains the
|
||||
face landmarks on each detected face.
|
||||
"""
|
||||
|
||||
return super().process(input_data={'image': image})
|
||||
@@ -0,0 +1,228 @@
|
||||
# Copyright 2020 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.
|
||||
|
||||
# Lint as: python3
|
||||
"""MediaPipe Hands."""
|
||||
|
||||
import enum
|
||||
from typing import NamedTuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
# pylint: disable=unused-import
|
||||
from mediapipe.calculators.core import gate_calculator_pb2
|
||||
from mediapipe.calculators.core import split_vector_calculator_pb2
|
||||
from mediapipe.calculators.tensor import image_to_tensor_calculator_pb2
|
||||
from mediapipe.calculators.tensor import inference_calculator_pb2
|
||||
from mediapipe.calculators.tensor import tensors_to_classification_calculator_pb2
|
||||
from mediapipe.calculators.tensor import tensors_to_detections_calculator_pb2
|
||||
from mediapipe.calculators.tensor import tensors_to_landmarks_calculator_pb2
|
||||
from mediapipe.calculators.tflite import ssd_anchors_calculator_pb2
|
||||
from mediapipe.calculators.util import association_calculator_pb2
|
||||
from mediapipe.calculators.util import detections_to_rects_calculator_pb2
|
||||
from mediapipe.calculators.util import logic_calculator_pb2
|
||||
from mediapipe.calculators.util import non_max_suppression_calculator_pb2
|
||||
from mediapipe.calculators.util import rect_transformation_calculator_pb2
|
||||
from mediapipe.calculators.util import thresholding_calculator_pb2
|
||||
# pylint: enable=unused-import
|
||||
from mediapipe.python.solution_base import SolutionBase
|
||||
|
||||
|
||||
class HandLandmark(enum.IntEnum):
|
||||
"""The 21 hand landmarks."""
|
||||
WRIST = 0
|
||||
THUMB_CMC = 1
|
||||
THUMB_MCP = 2
|
||||
THUMB_IP = 3
|
||||
THUMB_TIP = 4
|
||||
INDEX_FINGER_MCP = 5
|
||||
INDEX_FINGER_PIP = 6
|
||||
INDEX_FINGER_DIP = 7
|
||||
INDEX_FINGER_TIP = 8
|
||||
MIDDLE_FINGER_MCP = 9
|
||||
MIDDLE_FINGER_PIP = 10
|
||||
MIDDLE_FINGER_DIP = 11
|
||||
MIDDLE_FINGER_TIP = 12
|
||||
RING_FINGER_MCP = 13
|
||||
RING_FINGER_PIP = 14
|
||||
RING_FINGER_DIP = 15
|
||||
RING_FINGER_TIP = 16
|
||||
PINKY_MCP = 17
|
||||
PINKY_PIP = 18
|
||||
PINKY_DIP = 19
|
||||
PINKY_TIP = 20
|
||||
|
||||
|
||||
BINARYPB_FILE_PATH = 'mediapipe/modules/hand_landmark/hand_landmark_tracking_cpu.binarypb'
|
||||
HAND_CONNECTIONS = frozenset([
|
||||
(HandLandmark.WRIST, HandLandmark.THUMB_CMC),
|
||||
(HandLandmark.THUMB_CMC, HandLandmark.THUMB_MCP),
|
||||
(HandLandmark.THUMB_MCP, HandLandmark.THUMB_IP),
|
||||
(HandLandmark.THUMB_IP, HandLandmark.THUMB_TIP),
|
||||
(HandLandmark.WRIST, HandLandmark.INDEX_FINGER_MCP),
|
||||
(HandLandmark.INDEX_FINGER_MCP, HandLandmark.INDEX_FINGER_PIP),
|
||||
(HandLandmark.INDEX_FINGER_PIP, HandLandmark.INDEX_FINGER_DIP),
|
||||
(HandLandmark.INDEX_FINGER_DIP, HandLandmark.INDEX_FINGER_TIP),
|
||||
(HandLandmark.INDEX_FINGER_MCP, HandLandmark.MIDDLE_FINGER_MCP),
|
||||
(HandLandmark.MIDDLE_FINGER_MCP, HandLandmark.MIDDLE_FINGER_PIP),
|
||||
(HandLandmark.MIDDLE_FINGER_PIP, HandLandmark.MIDDLE_FINGER_DIP),
|
||||
(HandLandmark.MIDDLE_FINGER_DIP, HandLandmark.MIDDLE_FINGER_TIP),
|
||||
(HandLandmark.MIDDLE_FINGER_MCP, HandLandmark.RING_FINGER_MCP),
|
||||
(HandLandmark.RING_FINGER_MCP, HandLandmark.RING_FINGER_PIP),
|
||||
(HandLandmark.RING_FINGER_PIP, HandLandmark.RING_FINGER_DIP),
|
||||
(HandLandmark.RING_FINGER_DIP, HandLandmark.RING_FINGER_TIP),
|
||||
(HandLandmark.RING_FINGER_MCP, HandLandmark.PINKY_MCP),
|
||||
(HandLandmark.WRIST, HandLandmark.PINKY_MCP),
|
||||
(HandLandmark.PINKY_MCP, HandLandmark.PINKY_PIP),
|
||||
(HandLandmark.PINKY_PIP, HandLandmark.PINKY_DIP),
|
||||
(HandLandmark.PINKY_DIP, HandLandmark.PINKY_TIP)
|
||||
])
|
||||
|
||||
|
||||
class Hands(SolutionBase):
|
||||
"""MediaPipe Hands.
|
||||
|
||||
MediaPipe Hands processes an RGB image and returns the hand landmarks and
|
||||
handedness (left v.s. right hand) of each detected hand.
|
||||
|
||||
Note that it determines handedness assuming the input image is mirrored,
|
||||
i.e., taken with a front-facing/selfie camera (
|
||||
https://en.wikipedia.org/wiki/Front-facing_camera) with images flipped
|
||||
horizontally. If that is not the case, use, for instance, cv2.flip(image, 1)
|
||||
to flip the image first for a correct handedness output.
|
||||
|
||||
Usage examples:
|
||||
import cv2
|
||||
import mediapipe as mp
|
||||
mp_drawing = mp.solutions.drawing_utils
|
||||
mp_hands = mp.solutions.hands
|
||||
|
||||
# For static images:
|
||||
hands = mp_hands.Hands(
|
||||
static_image_mode=True,
|
||||
max_num_hands=2,
|
||||
min_detection_confidence=0.7)
|
||||
for idx, file in enumerate(file_list):
|
||||
# Read an image, flip it around y-axis for correct handedness output (see
|
||||
# above).
|
||||
image = cv2.flip(cv2.imread(file), 1)
|
||||
# Convert the BGR image to RGB before processing.
|
||||
results = hands.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
||||
|
||||
# Print handedness and draw hand landmarks on the image.
|
||||
print('handedness:', results.multi_handedness)
|
||||
if not results.multi_hand_landmarks:
|
||||
continue
|
||||
annotated_image = image.copy()
|
||||
for hand_landmarks in results.multi_hand_landmarks:
|
||||
print('hand_landmarks:', hand_landmarks)
|
||||
mp_drawing.draw_landmarks(
|
||||
annotated_image, hand_landmarks, mp_hands.HAND_CONNECTIONS)
|
||||
cv2.imwrite(
|
||||
'/tmp/annotated_image' + str(idx) + '.png', cv2.flip(image, 1))
|
||||
hands.close()
|
||||
|
||||
# For webcam input:
|
||||
hands = mp_hands.Hands(
|
||||
min_detection_confidence=0.7, min_tracking_confidence=0.5)
|
||||
cap = cv2.VideoCapture(0)
|
||||
while cap.isOpened():
|
||||
success, image = cap.read()
|
||||
if not success:
|
||||
break
|
||||
|
||||
# Flip the image horizontally for a later selfie-view display, and convert
|
||||
# the BGR image to RGB.
|
||||
image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB)
|
||||
# To improve performance, optionally mark the image as not writeable to
|
||||
# pass by reference.
|
||||
image.flags.writeable = False
|
||||
results = hands.process(image)
|
||||
|
||||
# Draw the hand annotations on the image.
|
||||
image.flags.writeable = True
|
||||
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
||||
if results.multi_hand_landmarks:
|
||||
for hand_landmarks in results.multi_hand_landmarks:
|
||||
mp_drawing.draw_landmarks(
|
||||
image, hand_landmarks, mp_hands.HAND_CONNECTIONS)
|
||||
cv2.imshow('MediaPipe Hands', image)
|
||||
if cv2.waitKey(5) & 0xFF == 27:
|
||||
break
|
||||
hands.close()
|
||||
cap.release()
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
static_image_mode=False,
|
||||
max_num_hands=2,
|
||||
min_detection_confidence=0.7,
|
||||
min_tracking_confidence=0.5):
|
||||
"""Initializes a MediaPipe Hand object.
|
||||
|
||||
Args:
|
||||
static_image_mode: If set to False, the solution treats the input images
|
||||
as a video stream. It will try to detect hands in the first input
|
||||
images, and upon a successful detection further localizes the hand
|
||||
landmarks. In subsequent images, once all "max_num_hands" hands are
|
||||
detected and the corresponding hand landmarks are localized, it simply
|
||||
tracks those landmarks without invoking another detection until it loses
|
||||
track of any of the hands. This reduces latency and is ideal for
|
||||
processing video frames. If set to True, hand detection runs on every
|
||||
input image, ideal for processing a batch of static, possibly unrelated,
|
||||
images. Default to False.
|
||||
max_num_hands: Maximum number of hands to detect. Default to 2.
|
||||
min_detection_confidence: Minimum confidence value ([0.0, 1.0]) from the
|
||||
hand detection model for the detection to be considered successful.
|
||||
Default to 0.7.
|
||||
min_tracking_confidence: Minimum confidence value ([0.0, 1.0]) from the
|
||||
landmark-tracking model for the hand landmarks to be considered tracked
|
||||
successfully, or otherwise hand detection will be invoked automatically
|
||||
on the next input image. Setting it to a higher value can increase
|
||||
robustness of the solution, at the expense of a higher latency. Ignored
|
||||
if "static_image_mode" is True, where hand detection simply runs on
|
||||
every image. Default to 0.5.
|
||||
"""
|
||||
super().__init__(
|
||||
binary_graph_path=BINARYPB_FILE_PATH,
|
||||
side_inputs={
|
||||
'num_hands': max_num_hands,
|
||||
'can_skip_detection': not static_image_mode,
|
||||
},
|
||||
calculator_params={
|
||||
'palmdetectioncpu__TensorsToDetectionsCalculator.min_score_thresh':
|
||||
min_detection_confidence,
|
||||
'handlandmarkcpu__ThresholdingCalculator.threshold':
|
||||
min_tracking_confidence,
|
||||
},
|
||||
outputs=['multi_hand_landmarks', 'multi_handedness'])
|
||||
|
||||
def process(self, image: np.ndarray) -> NamedTuple:
|
||||
"""Processes an RGB image and returns the hand landmarks and handedness of each detected hand.
|
||||
|
||||
Args:
|
||||
image: An RGB image represented as a numpy ndarray.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the underlying graph occurs any error.
|
||||
ValueError: If the input image is not three channel RGB.
|
||||
|
||||
Returns:
|
||||
A NamedTuple object with two fields: a "multi_hand_landmarks" field that
|
||||
contains the hand landmarks on each detected hand and a "multi_handedness"
|
||||
field that contains the handedness (left v.s. right hand) of the detected
|
||||
hand.
|
||||
"""
|
||||
|
||||
return super().process(input_data={'image': image})
|
||||
@@ -0,0 +1,213 @@
|
||||
# Copyright 2020 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.
|
||||
|
||||
# Lint as: python3
|
||||
"""MediaPipe Pose."""
|
||||
|
||||
import enum
|
||||
from typing import NamedTuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
# pylint: disable=unused-import
|
||||
from mediapipe.calculators.core import gate_calculator_pb2
|
||||
from mediapipe.calculators.core import split_vector_calculator_pb2
|
||||
from mediapipe.calculators.tensor import image_to_tensor_calculator_pb2
|
||||
from mediapipe.calculators.tensor import inference_calculator_pb2
|
||||
from mediapipe.calculators.tensor import tensors_to_classification_calculator_pb2
|
||||
from mediapipe.calculators.tensor import tensors_to_detections_calculator_pb2
|
||||
from mediapipe.calculators.tensor import tensors_to_landmarks_calculator_pb2
|
||||
from mediapipe.calculators.util import detections_to_rects_calculator_pb2
|
||||
from mediapipe.calculators.util import landmarks_smoothing_calculator_pb2
|
||||
from mediapipe.calculators.util import logic_calculator_pb2
|
||||
from mediapipe.calculators.util import non_max_suppression_calculator_pb2
|
||||
from mediapipe.calculators.util import rect_transformation_calculator_pb2
|
||||
from mediapipe.calculators.util import thresholding_calculator_pb2
|
||||
# pylint: enable=unused-import
|
||||
from mediapipe.python.solution_base import SolutionBase
|
||||
|
||||
|
||||
class PoseLandmark(enum.IntEnum):
|
||||
"""The 25 (upper-body) pose landmarks."""
|
||||
NOSE = 0
|
||||
RIGHT_EYE_INNER = 1
|
||||
RIGHT_EYE = 2
|
||||
RIGHT_EYE_OUTER = 3
|
||||
LEFT_EYE_INNER = 4
|
||||
LEFT_EYE = 5
|
||||
LEFT_EYE_OUTER = 6
|
||||
RIGHT_EAR = 7
|
||||
LEFT_EAR = 8
|
||||
MOUTH_RIGHT = 9
|
||||
MOUTH_LEFT = 10
|
||||
RIGHT_SHOULDER = 11
|
||||
LEFT_SHOULDER = 12
|
||||
RIGHT_ELBOW = 13
|
||||
LEFT_ELBOW = 14
|
||||
RIGHT_WRIST = 15
|
||||
LEFT_WRIST = 16
|
||||
RIGHT_PINKY = 17
|
||||
LEFT_PINKY = 18
|
||||
RIGHT_INDEX = 19
|
||||
LEFT_INDEX = 20
|
||||
RIGHT_THUMB = 21
|
||||
LEFT_THUMB = 22
|
||||
RIGHT_HIP = 23
|
||||
LEFT_HIP = 24
|
||||
|
||||
|
||||
BINARYPB_FILE_PATH = 'mediapipe/modules/pose_landmark/pose_landmark_upper_body_smoothed_cpu.binarypb'
|
||||
POSE_CONNECTIONS = frozenset([
|
||||
(PoseLandmark.NOSE, PoseLandmark.RIGHT_EYE_INNER),
|
||||
(PoseLandmark.RIGHT_EYE_INNER, PoseLandmark.RIGHT_EYE),
|
||||
(PoseLandmark.RIGHT_EYE, PoseLandmark.RIGHT_EYE_OUTER),
|
||||
(PoseLandmark.RIGHT_EYE_OUTER, PoseLandmark.RIGHT_EAR),
|
||||
(PoseLandmark.NOSE, PoseLandmark.LEFT_EYE_INNER),
|
||||
(PoseLandmark.LEFT_EYE_INNER, PoseLandmark.LEFT_EYE),
|
||||
(PoseLandmark.LEFT_EYE, PoseLandmark.LEFT_EYE_OUTER),
|
||||
(PoseLandmark.LEFT_EYE_OUTER, PoseLandmark.LEFT_EAR),
|
||||
(PoseLandmark.MOUTH_RIGHT, PoseLandmark.MOUTH_LEFT),
|
||||
(PoseLandmark.RIGHT_SHOULDER, PoseLandmark.LEFT_SHOULDER),
|
||||
(PoseLandmark.RIGHT_SHOULDER, PoseLandmark.RIGHT_ELBOW),
|
||||
(PoseLandmark.RIGHT_ELBOW, PoseLandmark.RIGHT_WRIST),
|
||||
(PoseLandmark.RIGHT_WRIST, PoseLandmark.RIGHT_PINKY),
|
||||
(PoseLandmark.RIGHT_WRIST, PoseLandmark.RIGHT_INDEX),
|
||||
(PoseLandmark.RIGHT_WRIST, PoseLandmark.RIGHT_THUMB),
|
||||
(PoseLandmark.RIGHT_PINKY, PoseLandmark.RIGHT_INDEX),
|
||||
(PoseLandmark.LEFT_SHOULDER, PoseLandmark.LEFT_ELBOW),
|
||||
(PoseLandmark.LEFT_ELBOW, PoseLandmark.LEFT_WRIST),
|
||||
(PoseLandmark.LEFT_WRIST, PoseLandmark.LEFT_PINKY),
|
||||
(PoseLandmark.LEFT_WRIST, PoseLandmark.LEFT_INDEX),
|
||||
(PoseLandmark.LEFT_WRIST, PoseLandmark.LEFT_THUMB),
|
||||
(PoseLandmark.LEFT_PINKY, PoseLandmark.LEFT_INDEX),
|
||||
(PoseLandmark.RIGHT_SHOULDER, PoseLandmark.RIGHT_HIP),
|
||||
(PoseLandmark.LEFT_SHOULDER, PoseLandmark.LEFT_HIP),
|
||||
(PoseLandmark.RIGHT_HIP, PoseLandmark.LEFT_HIP)
|
||||
])
|
||||
|
||||
|
||||
class Pose(SolutionBase):
|
||||
"""MediaPipe Pose.
|
||||
|
||||
MediaPipe Pose processes an RGB image and returns pose landmarks on the most
|
||||
prominent person detected.
|
||||
|
||||
Usage examples:
|
||||
import cv2
|
||||
import mediapipe as mp
|
||||
mp_drawing = mp.solutions.drawing_utils
|
||||
mp_pose = mp.solutions.pose
|
||||
|
||||
# For static images:
|
||||
pose = mp_pose.Pose(
|
||||
static_image_mode=True, min_detection_confidence=0.5)
|
||||
for idx, file in enumerate(file_list):
|
||||
image = cv2.imread(file)
|
||||
# Convert the BGR image to RGB before processing.
|
||||
results = pose.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
||||
|
||||
# Print and draw pose landmarks on the image.
|
||||
print(
|
||||
'nose landmark:',
|
||||
results.pose_landmarks.landmark[mp_pose.PoseLandmark.NOSE])
|
||||
annotated_image = image.copy()
|
||||
mp_drawing.draw_landmarks(
|
||||
annotated_image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS)
|
||||
cv2.imwrite('/tmp/annotated_image' + str(idx) + '.png', image)
|
||||
pose.close()
|
||||
|
||||
# For webcam input:
|
||||
pose = mp_pose.Pose(
|
||||
min_detection_confidence=0.5, min_tracking_confidence=0.5)
|
||||
cap = cv2.VideoCapture(0)
|
||||
while cap.isOpened():
|
||||
success, image = cap.read()
|
||||
if not success:
|
||||
break
|
||||
|
||||
# Flip the image horizontally for a later selfie-view display, and convert
|
||||
# the BGR image to RGB.
|
||||
image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB)
|
||||
# To improve performance, optionally mark the image as not writeable to
|
||||
# pass by reference.
|
||||
image.flags.writeable = False
|
||||
results = pose.process(image)
|
||||
|
||||
# Draw the pose annotation on the image.
|
||||
image.flags.writeable = True
|
||||
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
||||
mp_drawing.draw_landmarks(
|
||||
image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS)
|
||||
cv2.imshow('MediaPipe Pose', image)
|
||||
if cv2.waitKey(5) & 0xFF == 27:
|
||||
break
|
||||
pose.close()
|
||||
cap.release()
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
static_image_mode=False,
|
||||
min_detection_confidence=0.5,
|
||||
min_tracking_confidence=0.5):
|
||||
"""Initializes a MediaPipe Pose object.
|
||||
|
||||
Args:
|
||||
static_image_mode: If set to False, the solution treats the input images
|
||||
as a video stream. It will try to detect the most prominent person in
|
||||
the very first images, and upon a successful detection further localizes
|
||||
the pose landmarks. In subsequent images, it then simply tracks those
|
||||
landmarks without invoking another detection until it loses track, on
|
||||
reducing computation and latency. If set to True, person detection runs
|
||||
every input image, ideal for processing a batch of static, possibly
|
||||
unrelated, images. Default to False.
|
||||
min_detection_confidence: Minimum confidence value ([0.0, 1.0]) from the
|
||||
person-detection model for the detection to be considered successful.
|
||||
Default to 0.5.
|
||||
min_tracking_confidence: Minimum confidence value ([0.0, 1.0]) from the
|
||||
landmark-tracking model for the pose landmarks to be considered tracked
|
||||
successfully, or otherwise person detection will be invoked
|
||||
automatically on the next input image. Setting it to a higher value can
|
||||
increase robustness of the solution, at the expense of a higher latency.
|
||||
Ignored if "static_image_mode" is True, where person detection simply
|
||||
runs on every image. Default to 0.5.
|
||||
"""
|
||||
super().__init__(
|
||||
binary_graph_path=BINARYPB_FILE_PATH,
|
||||
side_inputs={
|
||||
'can_skip_detection': not static_image_mode,
|
||||
},
|
||||
calculator_params={
|
||||
'poselandmarkupperbodycpu__posedetectioncpu__TensorsToDetectionsCalculator.min_score_thresh':
|
||||
min_detection_confidence,
|
||||
'poselandmarkupperbodycpu__poselandmarkupperbodybyroicpu__ThresholdingCalculator.threshold':
|
||||
min_tracking_confidence,
|
||||
},
|
||||
outputs=['pose_landmarks'])
|
||||
|
||||
def process(self, image: np.ndarray) -> NamedTuple:
|
||||
"""Processes an RGB image and returns the pose landmarks on the most prominent person detected.
|
||||
|
||||
Args:
|
||||
image: An RGB image represented as a numpy ndarray.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the underlying graph occurs any error.
|
||||
ValueError: If the input image is not three channel RGB.
|
||||
|
||||
Returns:
|
||||
A NamedTuple object with a "pose_landmarks" field that contains the pose
|
||||
landmarks on the most prominent person detected.
|
||||
"""
|
||||
|
||||
return super().process(input_data={'image': image})
|
||||
Reference in New Issue
Block a user