Project import generated by Copybara.
GitOrigin-RevId: 6e4aff1cc351be3ae4537b677f36d139ee50ce09
This commit is contained in:
@@ -11,7 +11,6 @@
|
||||
# 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 SolutionBase module.
|
||||
|
||||
MediaPipe SolutionBase is the common base class for the high-level MediaPipe
|
||||
@@ -123,6 +122,8 @@ NAME_TO_TYPE: Mapping[str, '_PacketDataType'] = {
|
||||
_PacketDataType.PROTO,
|
||||
'::mediapipe::Landmark':
|
||||
_PacketDataType.PROTO,
|
||||
'::mediapipe::LandmarkList':
|
||||
_PacketDataType.PROTO,
|
||||
'::mediapipe::NormalizedLandmark':
|
||||
_PacketDataType.PROTO,
|
||||
'::mediapipe::FrameAnnotation':
|
||||
@@ -145,6 +146,8 @@ NAME_TO_TYPE: Mapping[str, '_PacketDataType'] = {
|
||||
_PacketDataType.PROTO_LIST,
|
||||
'::std::vector<::mediapipe::Landmark>':
|
||||
_PacketDataType.PROTO_LIST,
|
||||
'::std::vector<::mediapipe::LandmarkList>':
|
||||
_PacketDataType.PROTO_LIST,
|
||||
'::std::vector<::mediapipe::NormalizedLandmark>':
|
||||
_PacketDataType.PROTO_LIST,
|
||||
'::std::vector<::mediapipe::NormalizedLandmarkList>':
|
||||
|
||||
@@ -28,6 +28,8 @@ from mediapipe.framework.formats import landmark_pb2
|
||||
PRESENCE_THRESHOLD = 0.5
|
||||
RGB_CHANNELS = 3
|
||||
RED_COLOR = (0, 0, 255)
|
||||
GREEN_COLOR = (0, 128, 0)
|
||||
BLUE_COLOR = (255, 0, 0)
|
||||
VISIBILITY_THRESHOLD = 0.5
|
||||
|
||||
|
||||
@@ -178,9 +180,7 @@ def draw_axis(
|
||||
focal_length: Tuple[float, float] = (1.0, 1.0),
|
||||
principal_point: Tuple[float, float] = (0.0, 0.0),
|
||||
axis_length: float = 0.1,
|
||||
x_axis_drawing_spec: DrawingSpec = DrawingSpec(color=(0, 0, 255)),
|
||||
y_axis_drawing_spec: DrawingSpec = DrawingSpec(color=(0, 128, 0)),
|
||||
z_axis_drawing_spec: DrawingSpec = DrawingSpec(color=(255, 0, 0))):
|
||||
axis_drawing_spec: DrawingSpec = DrawingSpec()):
|
||||
"""Draws the 3D axis on the image.
|
||||
|
||||
Args:
|
||||
@@ -190,12 +190,8 @@ def draw_axis(
|
||||
focal_length: camera focal length along x and y directions.
|
||||
principal_point: camera principal point in x and y.
|
||||
axis_length: length of the axis in the drawing.
|
||||
x_axis_drawing_spec: A DrawingSpec object that specifies the x axis
|
||||
drawing settings such as color, line thickness.
|
||||
y_axis_drawing_spec: A DrawingSpec object that specifies the y axis
|
||||
drawing settings such as color, line thickness.
|
||||
z_axis_drawing_spec: A DrawingSpec object that specifies the z axis
|
||||
drawing settings such as color, line thickness.
|
||||
axis_drawing_spec: A DrawingSpec object that specifies the xyz axis
|
||||
drawing settings such as line thickness.
|
||||
|
||||
Raises:
|
||||
ValueError: If one of the followings:
|
||||
@@ -213,8 +209,8 @@ def draw_axis(
|
||||
# Project 3D points to NDC space.
|
||||
fx, fy = focal_length
|
||||
px, py = principal_point
|
||||
x_ndc = -fx * x / z + px
|
||||
y_ndc = -fy * y / z + py
|
||||
x_ndc = np.clip(-fx * x / (z + 1e-5) + px, -1., 1.)
|
||||
y_ndc = np.clip(-fy * y / (z + 1e-5) + py, -1., 1.)
|
||||
# Convert from NDC space to image space.
|
||||
x_im = np.int32((1 + x_ndc) * 0.5 * image_cols)
|
||||
y_im = np.int32((1 - y_ndc) * 0.5 * image_rows)
|
||||
@@ -223,9 +219,9 @@ def draw_axis(
|
||||
x_axis = (x_im[1], y_im[1])
|
||||
y_axis = (x_im[2], y_im[2])
|
||||
z_axis = (x_im[3], y_im[3])
|
||||
image = cv2.arrowedLine(image, origin, x_axis, x_axis_drawing_spec.color,
|
||||
x_axis_drawing_spec.thickness)
|
||||
image = cv2.arrowedLine(image, origin, y_axis, y_axis_drawing_spec.color,
|
||||
y_axis_drawing_spec.thickness)
|
||||
image = cv2.arrowedLine(image, origin, z_axis, z_axis_drawing_spec.color,
|
||||
z_axis_drawing_spec.thickness)
|
||||
cv2.arrowedLine(image, origin, x_axis, RED_COLOR,
|
||||
axis_drawing_spec.thickness)
|
||||
cv2.arrowedLine(image, origin, y_axis, GREEN_COLOR,
|
||||
axis_drawing_spec.thickness)
|
||||
cv2.arrowedLine(image, origin, z_axis, BLUE_COLOR,
|
||||
axis_drawing_spec.thickness)
|
||||
|
||||
@@ -28,6 +28,7 @@ from mediapipe.python.solutions import drawing_utils
|
||||
DEFAULT_BBOX_DRAWING_SPEC = drawing_utils.DrawingSpec()
|
||||
DEFAULT_CONNECTION_DRAWING_SPEC = drawing_utils.DrawingSpec()
|
||||
DEFAULT_CIRCLE_DRAWING_SPEC = drawing_utils.DrawingSpec(color=(0, 0, 255))
|
||||
DEFAULT_AXIS_DRAWING_SPEC = drawing_utils.DrawingSpec()
|
||||
|
||||
|
||||
class DrawingUtilTest(parameterized.TestCase):
|
||||
@@ -40,6 +41,11 @@ class DrawingUtilTest(parameterized.TestCase):
|
||||
with self.assertRaisesRegex(
|
||||
ValueError, 'Input image must contain three channel rgb data.'):
|
||||
drawing_utils.draw_detection(image, detection_pb2.Detection())
|
||||
with self.assertRaisesRegex(
|
||||
ValueError, 'Input image must contain three channel rgb data.'):
|
||||
rotation = np.eye(3, dtype=np.float32)
|
||||
translation = np.array([0., 0., 1.])
|
||||
drawing_utils.draw_axis(image, rotation, translation)
|
||||
|
||||
def test_invalid_connection(self):
|
||||
landmark_list = text_format.Parse(
|
||||
@@ -133,6 +139,43 @@ class DrawingUtilTest(parameterized.TestCase):
|
||||
image=image, landmark_list=landmark_list, connections=[(0, 1)])
|
||||
np.testing.assert_array_equal(image, expected_result)
|
||||
|
||||
def test_draw_axis(self):
|
||||
image = np.zeros((100, 100, 3), np.uint8)
|
||||
expected_result = np.copy(image)
|
||||
origin = (50, 50)
|
||||
x_axis = (75, 50)
|
||||
y_axis = (50, 22)
|
||||
z_axis = (50, 77)
|
||||
cv2.arrowedLine(expected_result, origin, x_axis, drawing_utils.RED_COLOR,
|
||||
DEFAULT_AXIS_DRAWING_SPEC.thickness)
|
||||
cv2.arrowedLine(expected_result, origin, y_axis, drawing_utils.GREEN_COLOR,
|
||||
DEFAULT_AXIS_DRAWING_SPEC.thickness)
|
||||
cv2.arrowedLine(expected_result, origin, z_axis, drawing_utils.BLUE_COLOR,
|
||||
DEFAULT_AXIS_DRAWING_SPEC.thickness)
|
||||
r = np.sqrt(2.) / 2.
|
||||
rotation = np.array([[1., 0., 0.], [0., r, -r], [0., r, r]])
|
||||
translation = np.array([0, 0, -0.2])
|
||||
drawing_utils.draw_axis(image, rotation, translation)
|
||||
np.testing.assert_array_equal(image, expected_result)
|
||||
|
||||
def test_draw_axis_zero_translation(self):
|
||||
image = np.zeros((100, 100, 3), np.uint8)
|
||||
expected_result = np.copy(image)
|
||||
origin = (50, 50)
|
||||
x_axis = (0, 50)
|
||||
y_axis = (50, 100)
|
||||
z_axis = (50, 50)
|
||||
cv2.arrowedLine(expected_result, origin, x_axis, drawing_utils.RED_COLOR,
|
||||
DEFAULT_AXIS_DRAWING_SPEC.thickness)
|
||||
cv2.arrowedLine(expected_result, origin, y_axis, drawing_utils.GREEN_COLOR,
|
||||
DEFAULT_AXIS_DRAWING_SPEC.thickness)
|
||||
cv2.arrowedLine(expected_result, origin, z_axis, drawing_utils.BLUE_COLOR,
|
||||
DEFAULT_AXIS_DRAWING_SPEC.thickness)
|
||||
rotation = np.eye(3, dtype=np.float32)
|
||||
translation = np.zeros((3,), dtype=np.float32)
|
||||
drawing_utils.draw_axis(image, rotation, translation)
|
||||
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}'
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
"""MediaPipe Objectron."""
|
||||
|
||||
import enum
|
||||
import os
|
||||
import shutil
|
||||
from typing import List, Tuple, NamedTuple, Optional
|
||||
import urllib.request
|
||||
|
||||
import attr
|
||||
import numpy as np
|
||||
@@ -89,6 +92,23 @@ BOX_CONNECTIONS = frozenset([
|
||||
(BoxLandmark.FRONT_BOTTOM_RIGHT, BoxLandmark.FRONT_TOP_RIGHT),
|
||||
(BoxLandmark.BACK_TOP_RIGHT, BoxLandmark.FRONT_TOP_RIGHT),
|
||||
])
|
||||
_OSS_URL_PREFIX = 'https://github.com/google/mediapipe/raw/master/'
|
||||
|
||||
|
||||
def _download_oss_model(model_path: str):
|
||||
"""Download the objectron oss model from GitHub if it doesn't exist in the package."""
|
||||
|
||||
mp_root_path = os.sep.join(os.path.abspath(__file__).split(os.sep)[:-4])
|
||||
model_abspath = os.path.join(mp_root_path, model_path)
|
||||
if os.path.exists(model_abspath):
|
||||
return
|
||||
model_url = _OSS_URL_PREFIX + model_path
|
||||
with urllib.request.urlopen(model_url) as response, open(model_abspath,
|
||||
'wb') as out_file:
|
||||
if response.code != 200:
|
||||
raise ConnectionError('Cannot download ' + model_path +
|
||||
' from the MediaPipe Github repo.')
|
||||
shutil.copyfileobj(response, out_file)
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
@@ -132,9 +152,10 @@ _MODEL_DICT = {
|
||||
}
|
||||
|
||||
|
||||
def GetModelByName(name: str) -> ObjectronModel:
|
||||
def get_model_by_name(name: str) -> ObjectronModel:
|
||||
if name not in _MODEL_DICT:
|
||||
raise ValueError(f'{name} is not a valid model name for Objectron.')
|
||||
_download_oss_model(_MODEL_DICT[name].model_path)
|
||||
return _MODEL_DICT[name]
|
||||
|
||||
|
||||
@@ -186,6 +207,10 @@ class Objectron(SolutionBase):
|
||||
conversions inside the API.
|
||||
image_size (Optional): size (image_width, image_height) of the input image
|
||||
, ONLY needed when use focal_length and principal_point in pixel space.
|
||||
|
||||
Raises:
|
||||
ConnectionError: If the objectron open source model can't be downloaded
|
||||
from the MediaPipe Github repo.
|
||||
"""
|
||||
# Get Camera parameters.
|
||||
fx, fy = focal_length
|
||||
@@ -199,7 +224,7 @@ class Objectron(SolutionBase):
|
||||
py = - (py - half_height) / half_height
|
||||
|
||||
# Create and init model.
|
||||
model = GetModelByName(model_name)
|
||||
model = get_model_by_name(model_name)
|
||||
super().__init__(
|
||||
binary_graph_path=BINARYPB_FILE_PATH,
|
||||
side_inputs={
|
||||
@@ -275,4 +300,3 @@ class Objectron(SolutionBase):
|
||||
new_outputs.append(ObjectronOutputs(landmarks_2d, landmarks_3d,
|
||||
rotation, translation, scale=scale))
|
||||
return new_outputs
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ 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
|
||||
from mediapipe.calculators.util import visibility_smoothing_calculator_pb2
|
||||
# pylint: enable=unused-import
|
||||
from mediapipe.python.solution_base import SolutionBase
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
# limitations under the License.
|
||||
"""Tests for mediapipe.python.solutions.pose."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from absl.testing import absltest
|
||||
from absl.testing import parameterized
|
||||
@@ -52,7 +54,7 @@ class PoseTest(parameterized.TestCase):
|
||||
|
||||
def _landmarks_list_to_array(self, landmark_list, image_shape):
|
||||
rows, cols, _ = image_shape
|
||||
return np.asarray([(lmk.x * cols, lmk.y * rows)
|
||||
return np.asarray([(lmk.x * cols, lmk.y * rows, lmk.z * cols)
|
||||
for lmk in landmark_list.landmark])
|
||||
|
||||
def _assert_diff_less(self, array1, array2, threshold):
|
||||
@@ -81,9 +83,9 @@ class PoseTest(parameterized.TestCase):
|
||||
for _ in range(num_frames):
|
||||
results = pose.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
||||
self._assert_diff_less(
|
||||
self._landmarks_list_to_array(results.pose_landmarks, image.shape),
|
||||
EXPECTED_UPPER_BODY_LANDMARKS,
|
||||
DIFF_THRESHOLD)
|
||||
self._landmarks_list_to_array(results.pose_landmarks,
|
||||
image.shape)[:, :2],
|
||||
EXPECTED_UPPER_BODY_LANDMARKS, DIFF_THRESHOLD)
|
||||
|
||||
@parameterized.named_parameters(('static_image_mode', True, 3),
|
||||
('video_mode', False, 3))
|
||||
@@ -95,9 +97,66 @@ class PoseTest(parameterized.TestCase):
|
||||
for _ in range(num_frames):
|
||||
results = pose.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
||||
self._assert_diff_less(
|
||||
self._landmarks_list_to_array(results.pose_landmarks, image.shape),
|
||||
EXPECTED_FULL_BODY_LANDMARKS,
|
||||
DIFF_THRESHOLD)
|
||||
self._landmarks_list_to_array(results.pose_landmarks,
|
||||
image.shape)[:, :2],
|
||||
EXPECTED_FULL_BODY_LANDMARKS, DIFF_THRESHOLD)
|
||||
|
||||
@parameterized.named_parameters(
|
||||
('full_body', False, 'pose_squats.full_body.npz'),
|
||||
('upper_body', True, 'pose_squats.upper_body.npz'))
|
||||
def test_on_video(self, upper_body_only, expected_name):
|
||||
"""Tests pose models on a video."""
|
||||
# If set to `True` will dump actual predictions to .npz and JSON files.
|
||||
dump_predictions = False
|
||||
|
||||
# Set threshold for comparing actual and expected predictions in pixels.
|
||||
diff_threshold = 50
|
||||
|
||||
video_path = os.path.join(os.path.dirname(__file__),
|
||||
'testdata/pose_squats.mp4')
|
||||
expected_path = os.path.join(os.path.dirname(__file__),
|
||||
'testdata/{}'.format(expected_name))
|
||||
|
||||
# Predict pose landmarks for each frame.
|
||||
video_cap = cv2.VideoCapture(video_path)
|
||||
actual_per_frame = []
|
||||
with mp_pose.Pose(
|
||||
static_image_mode=False, upper_body_only=upper_body_only) as pose:
|
||||
while True:
|
||||
# Get next frame of the video.
|
||||
success, input_frame = video_cap.read()
|
||||
if not success:
|
||||
break
|
||||
|
||||
# Run pose tracker.
|
||||
input_frame = cv2.cvtColor(input_frame, cv2.COLOR_BGR2RGB)
|
||||
result = pose.process(image=input_frame)
|
||||
pose_landmarks = self._landmarks_list_to_array(result.pose_landmarks,
|
||||
input_frame.shape)
|
||||
|
||||
actual_per_frame.append(pose_landmarks)
|
||||
actual = np.asarray(actual_per_frame)
|
||||
|
||||
if dump_predictions:
|
||||
# Dump .npz
|
||||
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
|
||||
np.savez(tmp_file, predictions=np.array(actual))
|
||||
print('Predictions saved as .npz to {}'.format(tmp_file.name))
|
||||
|
||||
# Dump JSON
|
||||
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
|
||||
with open(tmp_file.name, 'w') as fl:
|
||||
dump_data = {'predictions': np.around(actual, 3).tolist()}
|
||||
fl.write(json.dumps(dump_data, indent=2, separators=(',', ': ')))
|
||||
print('Predictions saved as JSON to {}'.format(tmp_file.name))
|
||||
|
||||
# Validate actual vs. expected predictions.
|
||||
expected = np.load(expected_path)['predictions']
|
||||
assert actual.shape == expected.shape, (
|
||||
'Unexpected shape of predictions: {} instead of {}'.format(
|
||||
actual.shape, expected.shape))
|
||||
self._assert_diff_less(
|
||||
actual[..., :2], expected[..., :2], threshold=diff_threshold)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user