From 3f68f90238a36521994c9ab5ee8500473f7f657d Mon Sep 17 00:00:00 2001 From: kinaryml Date: Wed, 12 Apr 2023 14:37:16 -0700 Subject: [PATCH 01/30] Deprecated output_type for the ImageSegmenter and InteractiveSegmenter APIs --- mediapipe/tasks/python/test/vision/BUILD | 19 ++ .../test/vision/image_segmenter_test.py | 194 ++++++++++++------ .../test/vision/interactive_segmenter_test.py | 29 ++- .../tasks/python/vision/image_segmenter.py | 96 ++++++--- .../python/vision/interactive_segmenter.py | 64 ++++-- mediapipe/tasks/testdata/vision/BUILD | 2 + 6 files changed, 272 insertions(+), 132 deletions(-) diff --git a/mediapipe/tasks/python/test/vision/BUILD b/mediapipe/tasks/python/test/vision/BUILD index 704e1af5..46838c26 100644 --- a/mediapipe/tasks/python/test/vision/BUILD +++ b/mediapipe/tasks/python/test/vision/BUILD @@ -93,6 +93,25 @@ py_test( ], ) +py_test( + name = "interactive_segmenter_test", + srcs = ["interactive_segmenter_test.py"], + data = [ + "//mediapipe/tasks/testdata/vision:test_images", + "//mediapipe/tasks/testdata/vision:test_models", + ], + deps = [ + "//mediapipe/python:_framework_bindings", + "//mediapipe/tasks/python/components/containers:keypoint", + "//mediapipe/tasks/python/components/containers:rect", + "//mediapipe/tasks/python/core:base_options", + "//mediapipe/tasks/python/test:test_utils", + "//mediapipe/tasks/python/vision:interactive_segmenter", + "//mediapipe/tasks/python/vision/core:image_processing_options", + "//mediapipe/tasks/python/vision/core:vision_task_running_mode", + ], +) + py_test( name = "face_detector_test", srcs = ["face_detector_test.py"], diff --git a/mediapipe/tasks/python/test/vision/image_segmenter_test.py b/mediapipe/tasks/python/test/vision/image_segmenter_test.py index 7f0b47eb..32792519 100644 --- a/mediapipe/tasks/python/test/vision/image_segmenter_test.py +++ b/mediapipe/tasks/python/test/vision/image_segmenter_test.py @@ -30,10 +30,10 @@ from mediapipe.tasks.python.test import test_utils from mediapipe.tasks.python.vision import image_segmenter from mediapipe.tasks.python.vision.core import vision_task_running_mode +ImageSegmenterResult = image_segmenter.ImageSegmenterResult _BaseOptions = base_options_module.BaseOptions _Image = image_module.Image _ImageFormat = image_frame.ImageFormat -_OutputType = image_segmenter.ImageSegmenterOptions.OutputType _Activation = image_segmenter.ImageSegmenterOptions.Activation _ImageSegmenter = image_segmenter.ImageSegmenter _ImageSegmenterOptions = image_segmenter.ImageSegmenterOptions @@ -42,11 +42,33 @@ _RUNNING_MODE = vision_task_running_mode.VisionTaskRunningMode _MODEL_FILE = 'deeplabv3.tflite' _IMAGE_FILE = 'segmentation_input_rotation0.jpg' _SEGMENTATION_FILE = 'segmentation_golden_rotation0.png' +_CAT_IMAGE = 'cat.jpg' +_CAT_MASK = 'cat_mask.jpg' _MASK_MAGNIFICATION_FACTOR = 10 _MASK_SIMILARITY_THRESHOLD = 0.98 _TEST_DATA_DIR = 'mediapipe/tasks/testdata/vision' +def _calculate_soft_iou(m1, m2): + intersection_sum = np.sum(m1 * m2) + union_sum = np.sum(m1 * m1) + np.sum(m2 * m2) - intersection_sum + + if union_sum > 0: + return intersection_sum / union_sum + else: + return 0 + + +def _similar_to_float_mask(actual_mask, expected_mask, similarity_threshold): + actual_mask = actual_mask.numpy_view() + expected_mask = expected_mask.numpy_view() / 255.0 + + return ( + actual_mask.shape == expected_mask.shape + and _calculate_soft_iou(actual_mask, expected_mask) > similarity_threshold + ) + + def _similar_to_uint8_mask(actual_mask, expected_mask): actual_mask_pixels = actual_mask.numpy_view().flatten() expected_mask_pixels = expected_mask.numpy_view().flatten() @@ -84,6 +106,14 @@ class ImageSegmenterTest(parameterized.TestCase): self.model_path = test_utils.get_test_data_path( os.path.join(_TEST_DATA_DIR, _MODEL_FILE)) + def _load_segmentation_mask(self, file_path: str): + # Loads ground truth segmentation file. + gt_segmentation_data = cv2.imread( + test_utils.get_test_data_path(os.path.join(_TEST_DATA_DIR, file_path)), + cv2.IMREAD_GRAYSCALE, + ) + return _Image(_ImageFormat.GRAY8, gt_segmentation_data) + def test_create_from_file_succeeds_with_valid_model_path(self): # Creates with default option and valid model file successfully. with _ImageSegmenter.create_from_model_path(self.model_path) as segmenter: @@ -127,20 +157,19 @@ class ImageSegmenterTest(parameterized.TestCase): raise ValueError('model_file_type is invalid.') options = _ImageSegmenterOptions( - base_options=base_options, output_type=_OutputType.CATEGORY_MASK) + base_options=base_options, output_category_mask=True) segmenter = _ImageSegmenter.create_from_options(options) # Performs image segmentation on the input. - category_masks = segmenter.segment(self.test_image) - self.assertLen(category_masks, 1) - category_mask = category_masks[0] + segmentation_result = segmenter.segment(self.test_image) + category_mask = segmentation_result.category_mask result_pixels = category_mask.numpy_view().flatten() # Check if data type of `category_mask` is correct. self.assertEqual(result_pixels.dtype, np.uint8) self.assertTrue( - _similar_to_uint8_mask(category_masks[0], self.test_seg_image), + _similar_to_uint8_mask(category_mask, self.test_seg_image), f'Number of pixels in the candidate mask differing from that of the ' f'ground truth mask exceeds {_MASK_SIMILARITY_THRESHOLD}.') @@ -152,67 +181,33 @@ class ImageSegmenterTest(parameterized.TestCase): # Creates segmenter. base_options = _BaseOptions(model_asset_path=self.model_path) - # Run segmentation on the model in CATEGORY_MASK mode. - options = _ImageSegmenterOptions( - base_options=base_options, output_type=_OutputType.CATEGORY_MASK) - segmenter = _ImageSegmenter.create_from_options(options) - category_masks = segmenter.segment(self.test_image) - category_mask = category_masks[0].numpy_view() + # Load the cat image. + test_image = _Image.create_from_file( + test_utils.get_test_data_path( + os.path.join(_TEST_DATA_DIR, _CAT_IMAGE))) # Run segmentation on the model in CONFIDENCE_MASK mode. options = _ImageSegmenterOptions( base_options=base_options, - output_type=_OutputType.CONFIDENCE_MASK, activation=_Activation.SOFTMAX) - segmenter = _ImageSegmenter.create_from_options(options) - confidence_masks = segmenter.segment(self.test_image) - # Check if confidence mask shape is correct. - self.assertLen( - confidence_masks, 21, - 'Number of confidence masks must match with number of categories.') - - # Gather the confidence masks in a single array `confidence_mask_array`. - confidence_mask_array = np.array( - [confidence_mask.numpy_view() for confidence_mask in confidence_masks]) - - # Check if data type of `confidence_masks` are correct. - self.assertEqual(confidence_mask_array.dtype, np.float32) - - # Compute the category mask from the created confidence mask. - calculated_category_mask = np.argmax(confidence_mask_array, axis=0) - self.assertListEqual( - calculated_category_mask.tolist(), category_mask.tolist(), - 'Confidence mask does not match with the category mask.') - - # Closes the segmenter explicitly when the segmenter is not used in - # a context. - segmenter.close() - - @parameterized.parameters((ModelFileType.FILE_NAME), - (ModelFileType.FILE_CONTENT)) - def test_segment_in_context(self, model_file_type): - if model_file_type is ModelFileType.FILE_NAME: - base_options = _BaseOptions(model_asset_path=self.model_path) - elif model_file_type is ModelFileType.FILE_CONTENT: - with open(self.model_path, 'rb') as f: - model_contents = f.read() - base_options = _BaseOptions(model_asset_buffer=model_contents) - else: - # Should never happen - raise ValueError('model_file_type is invalid.') - - options = _ImageSegmenterOptions( - base_options=base_options, output_type=_OutputType.CATEGORY_MASK) with _ImageSegmenter.create_from_options(options) as segmenter: - # Performs image segmentation on the input. - category_masks = segmenter.segment(self.test_image) - self.assertLen(category_masks, 1) + segmentation_result = segmenter.segment(test_image) + confidence_masks = segmentation_result.confidence_masks + + # Check if confidence mask shape is correct. + self.assertLen( + confidence_masks, 21, + 'Number of confidence masks must match with number of categories.') + + # Loads ground truth segmentation file. + expected_mask = self._load_segmentation_mask(_CAT_MASK) self.assertTrue( - _similar_to_uint8_mask(category_masks[0], self.test_seg_image), - f'Number of pixels in the candidate mask differing from that of the ' - f'ground truth mask exceeds {_MASK_SIMILARITY_THRESHOLD}.') + _similar_to_float_mask( + confidence_masks[8], expected_mask, _MASK_SIMILARITY_THRESHOLD + ) + ) def test_missing_result_callback(self): options = _ImageSegmenterOptions( @@ -280,20 +275,49 @@ class ImageSegmenterTest(parameterized.TestCase): ValueError, r'Input timestamp must be monotonically increasing'): segmenter.segment_for_video(self.test_image, 0) - def test_segment_for_video(self): + def test_segment_for_video_in_category_mask_mode(self): options = _ImageSegmenterOptions( base_options=_BaseOptions(model_asset_path=self.model_path), - output_type=_OutputType.CATEGORY_MASK, + output_category_mask=True, running_mode=_RUNNING_MODE.VIDEO) with _ImageSegmenter.create_from_options(options) as segmenter: for timestamp in range(0, 300, 30): - category_masks = segmenter.segment_for_video(self.test_image, timestamp) - self.assertLen(category_masks, 1) + segmentation_result = segmenter.segment_for_video( + self.test_image, timestamp) + category_mask = segmentation_result.category_mask self.assertTrue( - _similar_to_uint8_mask(category_masks[0], self.test_seg_image), + _similar_to_uint8_mask(category_mask, self.test_seg_image), f'Number of pixels in the candidate mask differing from that of the ' f'ground truth mask exceeds {_MASK_SIMILARITY_THRESHOLD}.') + def test_segment_for_video_in_confidence_mask_mode(self): + # Load the cat image. + test_image = _Image.create_from_file( + test_utils.get_test_data_path( + os.path.join(_TEST_DATA_DIR, _CAT_IMAGE))) + + options = _ImageSegmenterOptions( + base_options=_BaseOptions(model_asset_path=self.model_path), + running_mode=_RUNNING_MODE.VIDEO) + with _ImageSegmenter.create_from_options(options) as segmenter: + for timestamp in range(0, 300, 30): + segmentation_result = segmenter.segment_for_video( + test_image, timestamp) + confidence_masks = segmentation_result.confidence_masks + + # Check if confidence mask shape is correct. + self.assertLen( + confidence_masks, 21, + 'Number of confidence masks must match with number of categories.') + + # Loads ground truth segmentation file. + expected_mask = self._load_segmentation_mask(_CAT_MASK) + self.assertTrue( + _similar_to_float_mask( + confidence_masks[8], expected_mask, _MASK_SIMILARITY_THRESHOLD + ) + ) + def test_calling_segment_in_live_stream_mode(self): options = _ImageSegmenterOptions( base_options=_BaseOptions(model_asset_path=self.model_path), @@ -325,13 +349,13 @@ class ImageSegmenterTest(parameterized.TestCase): ValueError, r'Input timestamp must be monotonically increasing'): segmenter.segment_async(self.test_image, 0) - def test_segment_async_calls(self): + def test_segment_async_calls_in_category_mask_mode(self): observed_timestamp_ms = -1 - def check_result(result: List[image_module.Image], output_image: _Image, + def check_result(result: ImageSegmenterResult, output_image: _Image, timestamp_ms: int): # Get the output category mask. - category_mask = result[0] + category_mask = result.category_mask self.assertEqual(output_image.width, self.test_image.width) self.assertEqual(output_image.height, self.test_image.height) self.assertEqual(output_image.width, self.test_seg_image.width) @@ -345,13 +369,49 @@ class ImageSegmenterTest(parameterized.TestCase): options = _ImageSegmenterOptions( base_options=_BaseOptions(model_asset_path=self.model_path), - output_type=_OutputType.CATEGORY_MASK, + output_category_mask=True, running_mode=_RUNNING_MODE.LIVE_STREAM, result_callback=check_result) with _ImageSegmenter.create_from_options(options) as segmenter: for timestamp in range(0, 300, 30): segmenter.segment_async(self.test_image, timestamp) + def test_segment_async_calls_in_confidence_mask_mode(self): + # Load the cat image. + test_image = _Image.create_from_file( + test_utils.get_test_data_path( + os.path.join(_TEST_DATA_DIR, _CAT_IMAGE))) + + # Loads ground truth segmentation file. + expected_mask = self._load_segmentation_mask(_CAT_MASK) + observed_timestamp_ms = -1 + + def check_result(result: ImageSegmenterResult, output_image: _Image, + timestamp_ms: int): + # Get the output category mask. + confidence_masks = result.confidence_masks + + # Check if confidence mask shape is correct. + self.assertLen( + confidence_masks, 21, + 'Number of confidence masks must match with number of categories.') + + self.assertTrue( + _similar_to_float_mask( + confidence_masks[8], expected_mask, _MASK_SIMILARITY_THRESHOLD + ) + ) + self.assertLess(observed_timestamp_ms, timestamp_ms) + self.observed_timestamp_ms = timestamp_ms + + options = _ImageSegmenterOptions( + base_options=_BaseOptions(model_asset_path=self.model_path), + running_mode=_RUNNING_MODE.LIVE_STREAM, + result_callback=check_result) + with _ImageSegmenter.create_from_options(options) as segmenter: + for timestamp in range(0, 300, 30): + segmenter.segment_async(test_image, timestamp) + if __name__ == '__main__': absltest.main() diff --git a/mediapipe/tasks/python/test/vision/interactive_segmenter_test.py b/mediapipe/tasks/python/test/vision/interactive_segmenter_test.py index e8c52ae3..6af15aa0 100644 --- a/mediapipe/tasks/python/test/vision/interactive_segmenter_test.py +++ b/mediapipe/tasks/python/test/vision/interactive_segmenter_test.py @@ -30,12 +30,12 @@ from mediapipe.tasks.python.test import test_utils from mediapipe.tasks.python.vision import interactive_segmenter from mediapipe.tasks.python.vision.core import image_processing_options as image_processing_options_module +InteractiveSegmenterResult = interactive_segmenter.InteractiveSegmenterResult _BaseOptions = base_options_module.BaseOptions _Image = image_module.Image _ImageFormat = image_frame.ImageFormat _NormalizedKeypoint = keypoint_module.NormalizedKeypoint _Rect = rect.Rect -_OutputType = interactive_segmenter.InteractiveSegmenterOptions.OutputType _InteractiveSegmenter = interactive_segmenter.InteractiveSegmenter _InteractiveSegmenterOptions = interactive_segmenter.InteractiveSegmenterOptions _RegionOfInterest = interactive_segmenter.RegionOfInterest @@ -200,15 +200,14 @@ class InteractiveSegmenterTest(parameterized.TestCase): raise ValueError('model_file_type is invalid.') options = _InteractiveSegmenterOptions( - base_options=base_options, output_type=_OutputType.CATEGORY_MASK + base_options=base_options, output_category_mask=True ) segmenter = _InteractiveSegmenter.create_from_options(options) # Performs image segmentation on the input. roi = _RegionOfInterest(format=roi_format, keypoint=keypoint) - category_masks = segmenter.segment(self.test_image, roi) - self.assertLen(category_masks, 1) - category_mask = category_masks[0] + segmentation_result = segmenter.segment(self.test_image, roi) + category_mask = segmentation_result.category_mask result_pixels = category_mask.numpy_view().flatten() # Check if data type of `category_mask` is correct. @@ -219,7 +218,7 @@ class InteractiveSegmenterTest(parameterized.TestCase): self.assertTrue( _similar_to_uint8_mask( - category_masks[0], test_seg_image, similarity_threshold + category_mask, test_seg_image, similarity_threshold ), ( 'Number of pixels in the candidate mask differing from that of the' @@ -253,13 +252,12 @@ class InteractiveSegmenterTest(parameterized.TestCase): roi = _RegionOfInterest(format=roi_format, keypoint=keypoint) # Run segmentation on the model in CONFIDENCE_MASK mode. - options = _InteractiveSegmenterOptions( - base_options=base_options, output_type=_OutputType.CONFIDENCE_MASK - ) + options = _InteractiveSegmenterOptions(base_options=base_options) with _InteractiveSegmenter.create_from_options(options) as segmenter: # Perform segmentation - confidence_masks = segmenter.segment(self.test_image, roi) + segmentation_result = segmenter.segment(self.test_image, roi) + confidence_masks = segmentation_result.confidence_masks # Check if confidence mask shape is correct. self.assertLen( @@ -286,16 +284,15 @@ class InteractiveSegmenterTest(parameterized.TestCase): ) # Run segmentation on the model in CONFIDENCE_MASK mode. - options = _InteractiveSegmenterOptions( - base_options=base_options, output_type=_OutputType.CONFIDENCE_MASK - ) + options = _InteractiveSegmenterOptions(base_options=base_options) with _InteractiveSegmenter.create_from_options(options) as segmenter: # Perform segmentation image_processing_options = _ImageProcessingOptions(rotation_degrees=-90) - confidence_masks = segmenter.segment( + segmentation_result = segmenter.segment( self.test_image, roi, image_processing_options ) + confidence_masks = segmentation_result.confidence_masks # Check if confidence mask shape is correct. self.assertLen( @@ -313,9 +310,7 @@ class InteractiveSegmenterTest(parameterized.TestCase): ) # Run segmentation on the model in CONFIDENCE_MASK mode. - options = _InteractiveSegmenterOptions( - base_options=base_options, output_type=_OutputType.CONFIDENCE_MASK - ) + options = _InteractiveSegmenterOptions(base_options=base_options) with self.assertRaisesRegex( ValueError, "This task doesn't support region-of-interest." diff --git a/mediapipe/tasks/python/vision/image_segmenter.py b/mediapipe/tasks/python/vision/image_segmenter.py index e50ffbf7..10277317 100644 --- a/mediapipe/tasks/python/vision/image_segmenter.py +++ b/mediapipe/tasks/python/vision/image_segmenter.py @@ -31,7 +31,6 @@ from mediapipe.tasks.python.vision.core import base_vision_task_api from mediapipe.tasks.python.vision.core import image_processing_options as image_processing_options_module from mediapipe.tasks.python.vision.core import vision_task_running_mode -ImageSegmenterResult = List[image_module.Image] _NormalizedRect = rect.NormalizedRect _BaseOptions = base_options_module.BaseOptions _SegmenterOptionsProto = segmenter_options_pb2.SegmenterOptions @@ -42,8 +41,10 @@ _RunningMode = vision_task_running_mode.VisionTaskRunningMode _ImageProcessingOptions = image_processing_options_module.ImageProcessingOptions _TaskInfo = task_info_module.TaskInfo -_SEGMENTATION_OUT_STREAM_NAME = 'segmented_mask_out' -_SEGMENTATION_TAG = 'GROUPED_SEGMENTATION' +_CONFIDENCE_MASKS_STREAM_NAME = 'confidence_masks' +_CONFIDENCE_MASKS_TAG = 'CONFIDENCE_MASKS' +_CATEGORY_MASK_STREAM_NAME = 'category_mask' +_CATEGORY_MASK_TAG = 'CATEGORY_MASK' _IMAGE_IN_STREAM_NAME = 'image_in' _IMAGE_OUT_STREAM_NAME = 'image_out' _IMAGE_TAG = 'IMAGE' @@ -53,6 +54,12 @@ _TASK_GRAPH_NAME = 'mediapipe.tasks.vision.image_segmenter.ImageSegmenterGraph' _MICRO_SECONDS_PER_MILLISECOND = 1000 +@dataclasses.dataclass +class ImageSegmenterResult: + confidence_masks: Optional[List[image_module.Image]] = None + category_mask: Optional[image_module.Image] = None + + @dataclasses.dataclass class ImageSegmenterOptions: """Options for the image segmenter task. @@ -64,19 +71,13 @@ class ImageSegmenterOptions: objects on single image inputs. 2) The video mode for segmenting objects on the decoded frames of a video. 3) The live stream mode for segmenting objects on a live stream of input data, such as from camera. - output_type: The output mask type allows specifying the type of - post-processing to perform on the raw model results. - activation: Activation function to apply to input tensor. + output_confidence_masks: Whether to output confidence masks. + output_category_mask: Whether to output category mask. result_callback: The user-defined result callback for processing live stream data. The result callback should only be specified when the running mode is set to the live stream mode. """ - class OutputType(enum.Enum): - UNSPECIFIED = 0 - CATEGORY_MASK = 1 - CONFIDENCE_MASK = 2 - class Activation(enum.Enum): NONE = 0 SIGMOID = 1 @@ -84,7 +85,8 @@ class ImageSegmenterOptions: base_options: _BaseOptions running_mode: _RunningMode = _RunningMode.IMAGE - output_type: Optional[OutputType] = OutputType.CATEGORY_MASK + output_confidence_masks: bool = True + output_category_mask: bool = False activation: Optional[Activation] = Activation.NONE result_callback: Optional[ Callable[[ImageSegmenterResult, image_module.Image, int], None] @@ -98,7 +100,7 @@ class ImageSegmenterOptions: False if self.running_mode == _RunningMode.IMAGE else True ) segmenter_options_proto = _SegmenterOptionsProto( - output_type=self.output_type.value, activation=self.activation.value + activation=self.activation.value ) return _ImageSegmenterGraphOptionsProto( base_options=base_options_proto, @@ -177,27 +179,48 @@ class ImageSegmenter(base_vision_task_api.BaseVisionTaskApi): def packets_callback(output_packets: Mapping[str, packet.Packet]): if output_packets[_IMAGE_OUT_STREAM_NAME].is_empty(): return - segmentation_result = packet_getter.get_image_list( - output_packets[_SEGMENTATION_OUT_STREAM_NAME] - ) + + segmentation_result = ImageSegmenterResult() + + if options.output_confidence_masks: + segmentation_result.confidence_masks = packet_getter.get_image_list( + output_packets[_CONFIDENCE_MASKS_STREAM_NAME] + ) + + if options.output_category_mask: + segmentation_result.category_mask = packet_getter.get_image( + output_packets[_CATEGORY_MASK_STREAM_NAME] + ) + image = packet_getter.get_image(output_packets[_IMAGE_OUT_STREAM_NAME]) - timestamp = output_packets[_SEGMENTATION_OUT_STREAM_NAME].timestamp + timestamp = output_packets[_IMAGE_OUT_STREAM_NAME].timestamp options.result_callback( segmentation_result, image, timestamp.value // _MICRO_SECONDS_PER_MILLISECOND, ) + output_streams = [ + ':'.join([_IMAGE_TAG, _IMAGE_OUT_STREAM_NAME]), + ] + + if options.output_confidence_masks: + output_streams.append( + ':'.join([_CONFIDENCE_MASKS_TAG, _CONFIDENCE_MASKS_STREAM_NAME]) + ) + + if options.output_category_mask: + output_streams.append( + ':'.join([_CATEGORY_MASK_TAG, _CATEGORY_MASK_STREAM_NAME]) + ) + task_info = _TaskInfo( task_graph=_TASK_GRAPH_NAME, input_streams=[ ':'.join([_IMAGE_TAG, _IMAGE_IN_STREAM_NAME]), ':'.join([_NORM_RECT_TAG, _NORM_RECT_STREAM_NAME]), ], - output_streams=[ - ':'.join([_SEGMENTATION_TAG, _SEGMENTATION_OUT_STREAM_NAME]), - ':'.join([_IMAGE_TAG, _IMAGE_OUT_STREAM_NAME]), - ], + output_streams=output_streams, task_options=options, ) return cls( @@ -240,9 +263,18 @@ class ImageSegmenter(base_vision_task_api.BaseVisionTaskApi): normalized_rect.to_pb2() ), }) - segmentation_result = packet_getter.get_image_list( - output_packets[_SEGMENTATION_OUT_STREAM_NAME] - ) + segmentation_result = ImageSegmenterResult() + + if _CONFIDENCE_MASKS_STREAM_NAME in output_packets: + segmentation_result.confidence_masks = packet_getter.get_image_list( + output_packets[_CONFIDENCE_MASKS_STREAM_NAME] + ) + + if _CATEGORY_MASK_STREAM_NAME in output_packets: + segmentation_result.category_mask = packet_getter.get_image( + output_packets[_CATEGORY_MASK_STREAM_NAME] + ) + return segmentation_result def segment_for_video( @@ -285,9 +317,19 @@ class ImageSegmenter(base_vision_task_api.BaseVisionTaskApi): normalized_rect.to_pb2() ).at(timestamp_ms * _MICRO_SECONDS_PER_MILLISECOND), }) - segmentation_result = packet_getter.get_image_list( - output_packets[_SEGMENTATION_OUT_STREAM_NAME] - ) + segmentation_result = ImageSegmenterResult() + + if _CONFIDENCE_MASKS_STREAM_NAME in output_packets: + segmentation_result.confidence_masks = packet_getter.get_image_list( + output_packets[_CONFIDENCE_MASKS_STREAM_NAME] + ) + + if _CATEGORY_MASK_STREAM_NAME in output_packets: + segmentation_result.category_mask = packet_getter.get_image( + output_packets[_CATEGORY_MASK_STREAM_NAME] + ) + + return segmentation_result def segment_async( diff --git a/mediapipe/tasks/python/vision/interactive_segmenter.py b/mediapipe/tasks/python/vision/interactive_segmenter.py index 12a30b6e..6f423d76 100644 --- a/mediapipe/tasks/python/vision/interactive_segmenter.py +++ b/mediapipe/tasks/python/vision/interactive_segmenter.py @@ -41,8 +41,10 @@ _RunningMode = vision_task_running_mode.VisionTaskRunningMode _ImageProcessingOptions = image_processing_options_module.ImageProcessingOptions _TaskInfo = task_info_module.TaskInfo -_SEGMENTATION_OUT_STREAM_NAME = 'segmented_mask_out' -_SEGMENTATION_TAG = 'GROUPED_SEGMENTATION' +_CONFIDENCE_MASKS_STREAM_NAME = 'confidence_masks' +_CONFIDENCE_MASKS_TAG = 'CONFIDENCE_MASKS' +_CATEGORY_MASK_STREAM_NAME = 'category_mask' +_CATEGORY_MASK_TAG = 'CATEGORY_MASK' _IMAGE_IN_STREAM_NAME = 'image_in' _IMAGE_OUT_STREAM_NAME = 'image_out' _ROI_STREAM_NAME = 'roi_in' @@ -55,32 +57,32 @@ _TASK_GRAPH_NAME = ( ) +@dataclasses.dataclass +class InteractiveSegmenterResult: + confidence_masks: Optional[List[image_module.Image]] = None + category_mask: Optional[image_module.Image] = None + + @dataclasses.dataclass class InteractiveSegmenterOptions: """Options for the interactive segmenter task. Attributes: base_options: Base options for the interactive segmenter task. - output_type: The output mask type allows specifying the type of - post-processing to perform on the raw model results. + output_confidence_masks: Whether to output confidence masks. + output_category_mask: Whether to output category mask. """ - class OutputType(enum.Enum): - UNSPECIFIED = 0 - CATEGORY_MASK = 1 - CONFIDENCE_MASK = 2 - base_options: _BaseOptions - output_type: Optional[OutputType] = OutputType.CATEGORY_MASK + output_confidence_masks: bool = True + output_category_mask: bool = False @doc_controls.do_not_generate_docs def to_pb2(self) -> _ImageSegmenterGraphOptionsProto: """Generates an InteractiveSegmenterOptions protobuf object.""" base_options_proto = self.base_options.to_pb2() base_options_proto.use_stream_mode = False - segmenter_options_proto = _SegmenterOptionsProto( - output_type=self.output_type.value - ) + segmenter_options_proto = _SegmenterOptionsProto() return _ImageSegmenterGraphOptionsProto( base_options=base_options_proto, segmenter_options=segmenter_options_proto, @@ -192,6 +194,20 @@ class InteractiveSegmenter(base_vision_task_api.BaseVisionTaskApi): RuntimeError: If other types of error occurred. """ + output_streams = [ + ':'.join([_IMAGE_TAG, _IMAGE_OUT_STREAM_NAME]), + ] + + if options.output_confidence_masks: + output_streams.append( + ':'.join([_CONFIDENCE_MASKS_TAG, _CONFIDENCE_MASKS_STREAM_NAME]) + ) + + if options.output_category_mask: + output_streams.append( + ':'.join([_CATEGORY_MASK_TAG, _CATEGORY_MASK_STREAM_NAME]) + ) + task_info = _TaskInfo( task_graph=_TASK_GRAPH_NAME, input_streams=[ @@ -199,10 +215,7 @@ class InteractiveSegmenter(base_vision_task_api.BaseVisionTaskApi): ':'.join([_ROI_TAG, _ROI_STREAM_NAME]), ':'.join([_NORM_RECT_TAG, _NORM_RECT_STREAM_NAME]), ], - output_streams=[ - ':'.join([_SEGMENTATION_TAG, _SEGMENTATION_OUT_STREAM_NAME]), - ':'.join([_IMAGE_TAG, _IMAGE_OUT_STREAM_NAME]), - ], + output_streams=output_streams, task_options=options, ) return cls( @@ -216,7 +229,7 @@ class InteractiveSegmenter(base_vision_task_api.BaseVisionTaskApi): image: image_module.Image, roi: RegionOfInterest, image_processing_options: Optional[_ImageProcessingOptions] = None, - ) -> List[image_module.Image]: + ) -> InteractiveSegmenterResult: """Performs the actual segmentation task on the provided MediaPipe Image. The image can be of any size with format RGB. @@ -248,7 +261,16 @@ class InteractiveSegmenter(base_vision_task_api.BaseVisionTaskApi): normalized_rect.to_pb2() ), }) - segmentation_result = packet_getter.get_image_list( - output_packets[_SEGMENTATION_OUT_STREAM_NAME] - ) + segmentation_result = InteractiveSegmenterResult() + + if _CONFIDENCE_MASKS_STREAM_NAME in output_packets: + segmentation_result.confidence_masks = packet_getter.get_image_list( + output_packets[_CONFIDENCE_MASKS_STREAM_NAME] + ) + + if _CATEGORY_MASK_STREAM_NAME in output_packets: + segmentation_result.category_mask = packet_getter.get_image( + output_packets[_CATEGORY_MASK_STREAM_NAME] + ) + return segmentation_result diff --git a/mediapipe/tasks/testdata/vision/BUILD b/mediapipe/tasks/testdata/vision/BUILD index 0de0c255..23dc3ed5 100644 --- a/mediapipe/tasks/testdata/vision/BUILD +++ b/mediapipe/tasks/testdata/vision/BUILD @@ -77,6 +77,7 @@ mediapipe_files(srcs = [ "portrait_selfie_segmentation_landscape_expected_category_mask.jpg", "pose.jpg", "pose_detection.tflite", + "ptm_512_hdt_ptm_woid.tflite", "pose_landmark_lite.tflite", "pose_landmarker.task", "right_hands.jpg", @@ -187,6 +188,7 @@ filegroup( "mobilenet_v3_small_100_224_embedder.tflite", "palm_detection_full.tflite", "pose_detection.tflite", + "ptm_512_hdt_ptm_woid.tflite", "pose_landmark_lite.tflite", "pose_landmarker.task", "selfie_segm_128_128_3.tflite", From 2c154e20cc6d83d700ca79db0f65ebbed34cfe73 Mon Sep 17 00:00:00 2001 From: Prianka Liz Kariat Date: Thu, 13 Apr 2023 17:56:09 +0530 Subject: [PATCH 02/30] Updated all references of timestampMs to timestampInMilliseconds --- .../sources/MPPClassificationResult.h | 9 +++-- .../sources/MPPClassificationResult.m | 4 +- .../containers/sources/MPPEmbeddingResult.h | 10 ++--- .../containers/sources/MPPEmbeddingResult.m | 4 +- .../MPPClassificationResult+Helpers.mm | 6 +-- .../sources/MPPEmbeddingResult+Helpers.mm | 7 ++-- .../tasks/ios/core/sources/MPPTaskResult.h | 5 ++- .../tasks/ios/core/sources/MPPTaskResult.m | 6 +-- .../MPPImageClassifierTests.m | 20 ++++++---- .../sources/MPPTextClassifierResult.h | 4 +- .../sources/MPPTextClassifierResult.m | 4 +- .../MPPTextClassifierResult+Helpers.mm | 2 +- .../sources/MPPTextEmbedderResult.h | 4 +- .../sources/MPPTextEmbedderResult.m | 4 +- .../sources/MPPTextEmbedderResult+Helpers.mm | 2 +- .../sources/MPPImageClassifier.h | 37 ++++++++++--------- .../sources/MPPImageClassifier.mm | 31 ++++++++-------- .../sources/MPPImageClassifierResult.h | 4 +- .../sources/MPPImageClassifierResult.m | 4 +- .../MPPImageClassifierResult+Helpers.mm | 2 +- .../sources/MPPObjectDetectionResult.h | 4 +- .../sources/MPPObjectDetectionResult.m | 4 +- .../sources/MPPObjectDetector.h | 37 ++++++++++--------- .../sources/MPPObjectDetector.mm | 31 ++++++++-------- .../MPPObjectDetectionResult+Helpers.mm | 5 ++- 25 files changed, 131 insertions(+), 119 deletions(-) diff --git a/mediapipe/tasks/ios/components/containers/sources/MPPClassificationResult.h b/mediapipe/tasks/ios/components/containers/sources/MPPClassificationResult.h index cd464c6a..bbc9aa8a 100644 --- a/mediapipe/tasks/ios/components/containers/sources/MPPClassificationResult.h +++ b/mediapipe/tasks/ios/components/containers/sources/MPPClassificationResult.h @@ -90,7 +90,7 @@ NS_SWIFT_NAME(ClassificationResult) * amount of data to process might exceed the maximum size that the model can process: to solve * this, the input data is split into multiple chunks starting at different timestamps. */ -@property(nonatomic, readonly) NSInteger timestampMs; +@property(nonatomic, readonly) NSInteger timestampInMilliseconds; /** * Initializes a new `MPPClassificationResult` with the given array of classifications and time @@ -98,14 +98,15 @@ NS_SWIFT_NAME(ClassificationResult) * * @param classifications An Array of `MPPClassifications` objects containing the predicted * categories for each head of the model. - * @param timestampMs The timestamp (in milliseconds) of the start of the chunk of data + * @param timestampInMilliseconds The timestamp (in milliseconds) of the start of the chunk of data * corresponding to these results. * * @return An instance of `MPPClassificationResult` initialized with the given array of - * classifications and timestampMs. + * classifications and timestamp (in milliseconds). */ - (instancetype)initWithClassifications:(NSArray *)classifications - timestampMs:(NSInteger)timestampMs NS_DESIGNATED_INITIALIZER; + timestampInMilliseconds:(NSInteger)timestampInMilliseconds + NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; diff --git a/mediapipe/tasks/ios/components/containers/sources/MPPClassificationResult.m b/mediapipe/tasks/ios/components/containers/sources/MPPClassificationResult.m index 6d42d22c..8d944049 100644 --- a/mediapipe/tasks/ios/components/containers/sources/MPPClassificationResult.m +++ b/mediapipe/tasks/ios/components/containers/sources/MPPClassificationResult.m @@ -38,11 +38,11 @@ @implementation MPPClassificationResult - (instancetype)initWithClassifications:(NSArray *)classifications - timestampMs:(NSInteger)timestampMs { + timestampInMilliseconds:(NSInteger)timestampInMilliseconds { self = [super init]; if (self) { _classifications = classifications; - _timestampMs = timestampMs; + _timestampInMilliseconds = timestampInMilliseconds; } return self; diff --git a/mediapipe/tasks/ios/components/containers/sources/MPPEmbeddingResult.h b/mediapipe/tasks/ios/components/containers/sources/MPPEmbeddingResult.h index 8fd9b9df..4cfd8890 100644 --- a/mediapipe/tasks/ios/components/containers/sources/MPPEmbeddingResult.h +++ b/mediapipe/tasks/ios/components/containers/sources/MPPEmbeddingResult.h @@ -33,7 +33,7 @@ NS_SWIFT_NAME(EmbeddingResult) * cases, the amount of data to process might exceed the maximum size that the model can process. To * solve this, the input data is split into multiple chunks starting at different timestamps. */ -@property(nonatomic, readonly) NSInteger timestampMs; +@property(nonatomic, readonly) NSInteger timestampInMilliseconds; /** * Initializes a new `MPPEmbedding` with the given array of embeddings and timestamp (in @@ -41,14 +41,14 @@ NS_SWIFT_NAME(EmbeddingResult) * * @param embeddings An Array of `MPPEmbedding` objects containing the embedding results for each * head of the model. - * @param timestampMs The optional timestamp (in milliseconds) of the start of the chunk of data - * corresponding to these results. Pass `0` if timestamp is absent. + * @param timestampInMilliseconds The optional timestamp (in milliseconds) of the start of the chunk + * of data corresponding to these results. Pass `0` if timestamp is absent. * * @return An instance of `MPPEmbeddingResult` initialized with the given array of embeddings and - * timestampMs. + * timestamp (in milliseconds). */ - (instancetype)initWithEmbeddings:(NSArray *)embeddings - timestampMs:(NSInteger)timestampMs NS_DESIGNATED_INITIALIZER; + timestampInMilliseconds:(NSInteger)timestampInMilliseconds NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; diff --git a/mediapipe/tasks/ios/components/containers/sources/MPPEmbeddingResult.m b/mediapipe/tasks/ios/components/containers/sources/MPPEmbeddingResult.m index 56dd30fd..1f482858 100644 --- a/mediapipe/tasks/ios/components/containers/sources/MPPEmbeddingResult.m +++ b/mediapipe/tasks/ios/components/containers/sources/MPPEmbeddingResult.m @@ -17,11 +17,11 @@ @implementation MPPEmbeddingResult - (instancetype)initWithEmbeddings:(NSArray *)embeddings - timestampMs:(NSInteger)timestampMs { + timestampInMilliseconds:(NSInteger)timestampInMilliseconds { self = [super init]; if (self) { _embeddings = embeddings; - _timestampMs = timestampMs; + _timestampInMilliseconds = timestampInMilliseconds; } return self; diff --git a/mediapipe/tasks/ios/components/containers/utils/sources/MPPClassificationResult+Helpers.mm b/mediapipe/tasks/ios/components/containers/utils/sources/MPPClassificationResult+Helpers.mm index b02b032b..47f1cf45 100644 --- a/mediapipe/tasks/ios/components/containers/utils/sources/MPPClassificationResult+Helpers.mm +++ b/mediapipe/tasks/ios/components/containers/utils/sources/MPPClassificationResult+Helpers.mm @@ -55,13 +55,13 @@ using ClassificationResultProto = [classifications addObject:[MPPClassifications classificationsWithProto:classificationsProto]]; } - NSInteger timestampMs = 0; + NSInteger timestampInMilliseconds = 0; if (classificationResultProto.has_timestamp_ms()) { - timestampMs = (NSInteger)classificationResultProto.timestamp_ms(); + timestampInMilliseconds = (NSInteger)classificationResultProto.timestamp_ms(); } return [[MPPClassificationResult alloc] initWithClassifications:classifications - timestampMs:timestampMs]; + timestampInMilliseconds:timestampInMilliseconds]; ; } diff --git a/mediapipe/tasks/ios/components/containers/utils/sources/MPPEmbeddingResult+Helpers.mm b/mediapipe/tasks/ios/components/containers/utils/sources/MPPEmbeddingResult+Helpers.mm index f9863e9c..cf5569c0 100644 --- a/mediapipe/tasks/ios/components/containers/utils/sources/MPPEmbeddingResult+Helpers.mm +++ b/mediapipe/tasks/ios/components/containers/utils/sources/MPPEmbeddingResult+Helpers.mm @@ -31,12 +31,13 @@ using EmbeddingResultProto = ::mediapipe::tasks::components::containers::proto:: [embeddings addObject:[MPPEmbedding embeddingWithProto:embeddingProto]]; } - NSInteger timestampMs = 0; + NSInteger timestampInMilliseconds = 0; if (embeddingResultProto.has_timestamp_ms()) { - timestampMs = (NSInteger)embeddingResultProto.timestamp_ms(); + timestampInMilliseconds = (NSInteger)embeddingResultProto.timestamp_ms(); } - return [[MPPEmbeddingResult alloc] initWithEmbeddings:embeddings timestampMs:timestampMs]; + return [[MPPEmbeddingResult alloc] initWithEmbeddings:embeddings + timestampInMilliseconds:timestampInMilliseconds]; } @end diff --git a/mediapipe/tasks/ios/core/sources/MPPTaskResult.h b/mediapipe/tasks/ios/core/sources/MPPTaskResult.h index 4ee7b2fc..664a94ba 100644 --- a/mediapipe/tasks/ios/core/sources/MPPTaskResult.h +++ b/mediapipe/tasks/ios/core/sources/MPPTaskResult.h @@ -26,11 +26,12 @@ NS_SWIFT_NAME(TaskResult) /** * Timestamp that is associated with the task result object. */ -@property(nonatomic, assign, readonly) NSInteger timestampMs; +@property(nonatomic, assign, readonly) NSInteger timestampInMilliseconds; - (instancetype)init NS_UNAVAILABLE; -- (instancetype)initWithTimestampMs:(NSInteger)timestampMs NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithTimestampInMilliseconds:(NSInteger)timestampInMilliseconds + NS_DESIGNATED_INITIALIZER; @end diff --git a/mediapipe/tasks/ios/core/sources/MPPTaskResult.m b/mediapipe/tasks/ios/core/sources/MPPTaskResult.m index 6c08014f..8a7fa6b5 100644 --- a/mediapipe/tasks/ios/core/sources/MPPTaskResult.m +++ b/mediapipe/tasks/ios/core/sources/MPPTaskResult.m @@ -16,16 +16,16 @@ @implementation MPPTaskResult -- (instancetype)initWithTimestampMs:(NSInteger)timestampMs { +- (instancetype)initWithTimestampInMilliseconds:(NSInteger)timestampInMilliseconds { self = [super init]; if (self) { - _timestampMs = timestampMs; + _timestampInMilliseconds = timestampInMilliseconds; } return self; } - (id)copyWithZone:(NSZone *)zone { - return [[MPPTaskResult alloc] initWithTimestampMs:self.timestampMs]; + return [[MPPTaskResult alloc] initWithTimestampInMilliseconds:self.timestampInMilliseconds]; } @end diff --git a/mediapipe/tasks/ios/test/vision/image_classifier/MPPImageClassifierTests.m b/mediapipe/tasks/ios/test/vision/image_classifier/MPPImageClassifierTests.m index f7b26837..694a17ca 100644 --- a/mediapipe/tasks/ios/test/vision/image_classifier/MPPImageClassifierTests.m +++ b/mediapipe/tasks/ios/test/vision/image_classifier/MPPImageClassifierTests.m @@ -487,7 +487,7 @@ static NSString *const kExpectedErrorDomain = @"com.google.mediapipe.tasks"; NSError *liveStreamApiCallError; XCTAssertFalse([imageClassifier classifyAsyncImage:image - timestampMs:0 + timestampInMilliseconds:0 error:&liveStreamApiCallError]); NSError *expectedLiveStreamApiCallError = @@ -501,7 +501,9 @@ static NSString *const kExpectedErrorDomain = @"com.google.mediapipe.tasks"; AssertEqualErrors(liveStreamApiCallError, expectedLiveStreamApiCallError); NSError *videoApiCallError; - XCTAssertFalse([imageClassifier classifyVideoFrame:image timestampMs:0 error:&videoApiCallError]); + XCTAssertFalse([imageClassifier classifyVideoFrame:image + timestampInMilliseconds:0 + error:&videoApiCallError]); NSError *expectedVideoApiCallError = [NSError errorWithDomain:kExpectedErrorDomain @@ -524,7 +526,7 @@ static NSString *const kExpectedErrorDomain = @"com.google.mediapipe.tasks"; NSError *liveStreamApiCallError; XCTAssertFalse([imageClassifier classifyAsyncImage:image - timestampMs:0 + timestampInMilliseconds:0 error:&liveStreamApiCallError]); NSError *expectedLiveStreamApiCallError = @@ -575,7 +577,9 @@ static NSString *const kExpectedErrorDomain = @"com.google.mediapipe.tasks"; AssertEqualErrors(imageApiCallError, expectedImageApiCallError); NSError *videoApiCallError; - XCTAssertFalse([imageClassifier classifyVideoFrame:image timestampMs:0 error:&videoApiCallError]); + XCTAssertFalse([imageClassifier classifyVideoFrame:image + timestampInMilliseconds:0 + error:&videoApiCallError]); NSError *expectedVideoApiCallError = [NSError errorWithDomain:kExpectedErrorDomain @@ -601,7 +605,7 @@ static NSString *const kExpectedErrorDomain = @"com.google.mediapipe.tasks"; for (int i = 0; i < 3; i++) { MPPImageClassifierResult *imageClassifierResult = [imageClassifier classifyVideoFrame:image - timestampMs:i + timestampInMilliseconds:i error:nil]; [self assertImageClassifierResult:imageClassifierResult hasExpectedCategoriesCount:maxResults @@ -630,10 +634,10 @@ static NSString *const kExpectedErrorDomain = @"com.google.mediapipe.tasks"; MPPImage *image = [self imageWithFileInfo:kBurgerImage]; - XCTAssertTrue([imageClassifier classifyAsyncImage:image timestampMs:1 error:nil]); + XCTAssertTrue([imageClassifier classifyAsyncImage:image timestampInMilliseconds:1 error:nil]); NSError *error; - XCTAssertFalse([imageClassifier classifyAsyncImage:image timestampMs:0 error:&error]); + XCTAssertFalse([imageClassifier classifyAsyncImage:image timestampInMilliseconds:0 error:&error]); NSError *expectedError = [NSError errorWithDomain:kExpectedErrorDomain @@ -668,7 +672,7 @@ static NSString *const kExpectedErrorDomain = @"com.google.mediapipe.tasks"; MPPImage *image = [self imageWithFileInfo:kBurgerImage]; for (int i = 0; i < 3; i++) { - XCTAssertTrue([imageClassifier classifyAsyncImage:image timestampMs:i error:nil]); + XCTAssertTrue([imageClassifier classifyAsyncImage:image timestampInMilliseconds:i error:nil]); } } diff --git a/mediapipe/tasks/ios/text/text_classifier/sources/MPPTextClassifierResult.h b/mediapipe/tasks/ios/text/text_classifier/sources/MPPTextClassifierResult.h index 6744a8e1..9ce7fcec 100644 --- a/mediapipe/tasks/ios/text/text_classifier/sources/MPPTextClassifierResult.h +++ b/mediapipe/tasks/ios/text/text_classifier/sources/MPPTextClassifierResult.h @@ -31,13 +31,13 @@ NS_SWIFT_NAME(TextClassifierResult) * * @param classificationResult The `MPPClassificationResult` instance containing one set of results * per classifier head. - * @param timestampMs The timestamp for this result. + * @param timestampInMilliseconds The timestamp (in milliseconds) for this result. * * @return An instance of `MPPTextClassifierResult` initialized with the given * `MPPClassificationResult` and timestamp (in milliseconds). */ - (instancetype)initWithClassificationResult:(MPPClassificationResult *)classificationResult - timestampMs:(NSInteger)timestampMs; + timestampInMilliseconds:(NSInteger)timestampInMilliseconds; @end diff --git a/mediapipe/tasks/ios/text/text_classifier/sources/MPPTextClassifierResult.m b/mediapipe/tasks/ios/text/text_classifier/sources/MPPTextClassifierResult.m index 4d5c1104..09a2097c 100644 --- a/mediapipe/tasks/ios/text/text_classifier/sources/MPPTextClassifierResult.m +++ b/mediapipe/tasks/ios/text/text_classifier/sources/MPPTextClassifierResult.m @@ -17,8 +17,8 @@ @implementation MPPTextClassifierResult - (instancetype)initWithClassificationResult:(MPPClassificationResult *)classificationResult - timestampMs:(NSInteger)timestampMs { - self = [super initWithTimestampMs:timestampMs]; + timestampInMilliseconds:(NSInteger)timestampInMilliseconds { + self = [super initWithTimestampInMilliseconds:timestampInMilliseconds]; if (self) { _classificationResult = classificationResult; } diff --git a/mediapipe/tasks/ios/text/text_classifier/utils/sources/MPPTextClassifierResult+Helpers.mm b/mediapipe/tasks/ios/text/text_classifier/utils/sources/MPPTextClassifierResult+Helpers.mm index f5d6aa1d..5a924016 100644 --- a/mediapipe/tasks/ios/text/text_classifier/utils/sources/MPPTextClassifierResult+Helpers.mm +++ b/mediapipe/tasks/ios/text/text_classifier/utils/sources/MPPTextClassifierResult+Helpers.mm @@ -35,7 +35,7 @@ using ::mediapipe::Packet; return [[MPPTextClassifierResult alloc] initWithClassificationResult:classificationResult - timestampMs:(NSInteger)(packet.Timestamp().Value() / + timestampInMilliseconds:(NSInteger)(packet.Timestamp().Value() / kMicroSecondsPerMilliSecond)]; } diff --git a/mediapipe/tasks/ios/text/text_embedder/sources/MPPTextEmbedderResult.h b/mediapipe/tasks/ios/text/text_embedder/sources/MPPTextEmbedderResult.h index e4697dce..ab8edd16 100644 --- a/mediapipe/tasks/ios/text/text_embedder/sources/MPPTextEmbedderResult.h +++ b/mediapipe/tasks/ios/text/text_embedder/sources/MPPTextEmbedderResult.h @@ -31,13 +31,13 @@ NS_SWIFT_NAME(TextEmbedderResult) * * @param embeddingResult The `MPPEmbeddingResult` instance containing one set of results * per classifier head. - * @param timestampMs The timestamp for this result. + * @param timestampInMilliseconds The timestamp (in millisecondss) for this result. * * @return An instance of `MPPTextEmbedderResult` initialized with the given * `MPPEmbeddingResult` and timestamp (in milliseconds). */ - (instancetype)initWithEmbeddingResult:(MPPEmbeddingResult *)embeddingResult - timestampMs:(NSInteger)timestampMs; + timestampInMilliseconds:(NSInteger)timestampInMilliseconds; - (instancetype)init NS_UNAVAILABLE; diff --git a/mediapipe/tasks/ios/text/text_embedder/sources/MPPTextEmbedderResult.m b/mediapipe/tasks/ios/text/text_embedder/sources/MPPTextEmbedderResult.m index 5483e3c3..d764f63d 100644 --- a/mediapipe/tasks/ios/text/text_embedder/sources/MPPTextEmbedderResult.m +++ b/mediapipe/tasks/ios/text/text_embedder/sources/MPPTextEmbedderResult.m @@ -17,8 +17,8 @@ @implementation MPPTextEmbedderResult - (instancetype)initWithEmbeddingResult:(MPPEmbeddingResult *)embeddingResult - timestampMs:(NSInteger)timestampMs { - self = [super initWithTimestampMs:timestampMs]; + timestampInMilliseconds:(NSInteger)timestampInMilliseconds { + self = [super initWithTimestampInMilliseconds:timestampInMilliseconds]; if (self) { _embeddingResult = embeddingResult; } diff --git a/mediapipe/tasks/ios/text/text_embedder/utils/sources/MPPTextEmbedderResult+Helpers.mm b/mediapipe/tasks/ios/text/text_embedder/utils/sources/MPPTextEmbedderResult+Helpers.mm index b769292c..3534ea66 100644 --- a/mediapipe/tasks/ios/text/text_embedder/utils/sources/MPPTextEmbedderResult+Helpers.mm +++ b/mediapipe/tasks/ios/text/text_embedder/utils/sources/MPPTextEmbedderResult+Helpers.mm @@ -34,7 +34,7 @@ using ::mediapipe::Packet; return [[MPPTextEmbedderResult alloc] initWithEmbeddingResult:embeddingResult - timestampMs:(NSInteger)(packet.Timestamp().Value() / + timestampInMilliseconds:(NSInteger)(packet.Timestamp().Value() / kMicroSecondsPerMilliSecond)]; } diff --git a/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifier.h b/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifier.h index 581c8d95..34568787 100644 --- a/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifier.h +++ b/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifier.h @@ -122,17 +122,17 @@ NS_SWIFT_NAME(ImageClassifier) * `MPPRunningModeVideo`. * * @param image The `MPPImage` on which image classification is to be performed. - * @param timestampMs The video frame's timestamp (in milliseconds). The input timestamps must be - * monotonically increasing. + * @param timestampInMilliseconds The video frame's timestamp (in milliseconds). The input + * timestamps must be monotonically increasing. * @param error An optional error parameter populated when there is an error in performing image * classification on the input video frame. * * @return An `MPPImageClassifierResult` object that contains a list of image classifications. */ - (nullable MPPImageClassifierResult *)classifyVideoFrame:(MPPImage *)image - timestampMs:(NSInteger)timestampMs + timestampInMilliseconds:(NSInteger)timestampInMilliseconds error:(NSError **)error - NS_SWIFT_NAME(classify(videoFrame:timestampMs:)); + NS_SWIFT_NAME(classify(videoFrame:timestampInMilliseconds:)); /** * Performs image classification on the provided video frame of type `MPPImage` cropped to the @@ -145,8 +145,8 @@ NS_SWIFT_NAME(ImageClassifier) * * @param image A live stream image data of type `MPPImage` on which image classification is to be * performed. - * @param timestampMs The video frame's timestamp (in milliseconds). The input timestamps must be - * monotonically increasing. + * @param timestampInMilliseconds The video frame's timestamp (in milliseconds). The input + * timestamps must be monotonically increasing. * @param roi A `CGRect` specifying the region of interest within the video frame of type * `MPPImage`, on which image classification should be performed. * @param error An optional error parameter populated when there is an error in performing image @@ -155,10 +155,10 @@ NS_SWIFT_NAME(ImageClassifier) * @return An `MPPImageClassifierResult` object that contains a list of image classifications. */ - (nullable MPPImageClassifierResult *)classifyVideoFrame:(MPPImage *)image - timestampMs:(NSInteger)timestampMs + timestampInMilliseconds:(NSInteger)timestampInMilliseconds regionOfInterest:(CGRect)roi error:(NSError **)error - NS_SWIFT_NAME(classify(videoFrame:timestampMs:regionOfInterest:)); + NS_SWIFT_NAME(classify(videoFrame:timestampInMilliseconds:regionOfInterest:)); /** * Sends live stream image data of type `MPPImage` to perform image classification using the whole @@ -172,16 +172,17 @@ NS_SWIFT_NAME(ImageClassifier) * * @param image A live stream image data of type `MPPImage` on which image classification is to be * performed. - * @param timestampMs The timestamp (in milliseconds) which indicates when the input image is sent - * to the image classifier. The input timestamps must be monotonically increasing. + * @param timestampInMilliseconds The timestamp (in milliseconds) which indicates when the input + * image is sent to the image classifier. The input timestamps must be monotonically increasing. * @param error An optional error parameter populated when there is an error in performing image * classification on the input live stream image data. * * @return `YES` if the image was sent to the task successfully, otherwise `NO`. */ - (BOOL)classifyAsyncImage:(MPPImage *)image - timestampMs:(NSInteger)timestampMs - error:(NSError **)error NS_SWIFT_NAME(classifyAsync(image:timestampMs:)); + timestampInMilliseconds:(NSInteger)timestampInMilliseconds + error:(NSError **)error + NS_SWIFT_NAME(classifyAsync(image:timestampInMilliseconds:)); /** * Sends live stream image data of type `MPPImage` to perform image classification, cropped to the @@ -195,8 +196,8 @@ NS_SWIFT_NAME(ImageClassifier) * * @param image A live stream image data of type `MPPImage` on which image classification is to be * performed. - * @param timestampMs The timestamp (in milliseconds) which indicates when the input image is sent - * to the image classifier. The input timestamps must be monotonically increasing. + * @param timestampInMilliseconds The timestamp (in milliseconds) which indicates when the input + * image is sent to the image classifier. The input timestamps must be monotonically increasing. * @param roi A `CGRect` specifying the region of interest within the given live stream image data * of type `MPPImage`, on which image classification should be performed. * @param error An optional error parameter populated when there is an error in performing image @@ -205,10 +206,10 @@ NS_SWIFT_NAME(ImageClassifier) * @return `YES` if the image was sent to the task successfully, otherwise `NO`. */ - (BOOL)classifyAsyncImage:(MPPImage *)image - timestampMs:(NSInteger)timestampMs - regionOfInterest:(CGRect)roi - error:(NSError **)error - NS_SWIFT_NAME(classifyAsync(image:timestampMs:regionOfInterest:)); + timestampInMilliseconds:(NSInteger)timestampInMilliseconds + regionOfInterest:(CGRect)roi + error:(NSError **)error + NS_SWIFT_NAME(classifyAsync(image:timestampInMilliseconds:regionOfInterest:)); - (instancetype)init NS_UNAVAILABLE; diff --git a/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifier.mm b/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifier.mm index 8051fbf3..18c1bb56 100644 --- a/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifier.mm +++ b/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifier.mm @@ -149,7 +149,7 @@ static NSString *const kTaskGraphName = } - (std::optional)inputPacketMapWithMPPImage:(MPPImage *)image - timestampMs:(NSInteger)timestampMs + timestampInMilliseconds:(NSInteger)timestampInMilliseconds regionOfInterest:(CGRect)roi error:(NSError **)error { std::optional rect = @@ -162,14 +162,15 @@ static NSString *const kTaskGraphName = } Packet imagePacket = [MPPVisionPacketCreator createPacketWithMPPImage:image - timestampMs:timestampMs + timestampInMilliseconds:timestampInMilliseconds error:error]; if (imagePacket.IsEmpty()) { return std::nullopt; } - Packet normalizedRectPacket = [MPPVisionPacketCreator createPacketWithNormalizedRect:rect.value() - timestampMs:timestampMs]; + Packet normalizedRectPacket = + [MPPVisionPacketCreator createPacketWithNormalizedRect:rect.value() + timestampInMilliseconds:timestampInMilliseconds]; PacketMap inputPacketMap = InputPacketMap(imagePacket, normalizedRectPacket); return inputPacketMap; @@ -180,11 +181,11 @@ static NSString *const kTaskGraphName = } - (nullable MPPImageClassifierResult *)classifyVideoFrame:(MPPImage *)image - timestampMs:(NSInteger)timestampMs + timestampInMilliseconds:(NSInteger)timestampInMilliseconds regionOfInterest:(CGRect)roi error:(NSError **)error { std::optional inputPacketMap = [self inputPacketMapWithMPPImage:image - timestampMs:timestampMs + timestampInMilliseconds:timestampInMilliseconds regionOfInterest:roi error:error]; if (!inputPacketMap.has_value()) { @@ -204,20 +205,20 @@ static NSString *const kTaskGraphName = } - (nullable MPPImageClassifierResult *)classifyVideoFrame:(MPPImage *)image - timestampMs:(NSInteger)timestampMs + timestampInMilliseconds:(NSInteger)timestampInMilliseconds error:(NSError **)error { return [self classifyVideoFrame:image - timestampMs:timestampMs + timestampInMilliseconds:timestampInMilliseconds regionOfInterest:CGRectZero error:error]; } - (BOOL)classifyAsyncImage:(MPPImage *)image - timestampMs:(NSInteger)timestampMs - regionOfInterest:(CGRect)roi - error:(NSError **)error { + timestampInMilliseconds:(NSInteger)timestampInMilliseconds + regionOfInterest:(CGRect)roi + error:(NSError **)error { std::optional inputPacketMap = [self inputPacketMapWithMPPImage:image - timestampMs:timestampMs + timestampInMilliseconds:timestampInMilliseconds regionOfInterest:roi error:error]; if (!inputPacketMap.has_value()) { @@ -228,10 +229,10 @@ static NSString *const kTaskGraphName = } - (BOOL)classifyAsyncImage:(MPPImage *)image - timestampMs:(NSInteger)timestampMs - error:(NSError **)error { + timestampInMilliseconds:(NSInteger)timestampInMilliseconds + error:(NSError **)error { return [self classifyAsyncImage:image - timestampMs:timestampMs + timestampInMilliseconds:timestampInMilliseconds regionOfInterest:CGRectZero error:error]; } diff --git a/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierResult.h b/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierResult.h index 92fdb13c..478bd452 100644 --- a/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierResult.h +++ b/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierResult.h @@ -31,13 +31,13 @@ NS_SWIFT_NAME(ImageClassifierResult) * * @param classificationResult The `MPPClassificationResult` instance containing one set of results * per classifier head. - * @param timestampMs The timestamp for this result. + * @param timestampInMilliseconds The timestamp (in milliseconds) for this result. * * @return An instance of `MPPImageClassifierResult` initialized with the given * `MPPClassificationResult` and timestamp (in milliseconds). */ - (instancetype)initWithClassificationResult:(MPPClassificationResult *)classificationResult - timestampMs:(NSInteger)timestampMs; + timestampInMilliseconds:(NSInteger)timestampInMilliseconds; @end diff --git a/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierResult.m b/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierResult.m index 6dcd064e..cb17bb10 100644 --- a/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierResult.m +++ b/mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifierResult.m @@ -17,8 +17,8 @@ @implementation MPPImageClassifierResult - (instancetype)initWithClassificationResult:(MPPClassificationResult *)classificationResult - timestampMs:(NSInteger)timestampMs { - self = [super initWithTimestampMs:timestampMs]; + timestampInMilliseconds:(NSInteger)timestampInMilliseconds { + self = [super initWithTimestampInMilliseconds:timestampInMilliseconds]; if (self) { _classificationResult = classificationResult; } diff --git a/mediapipe/tasks/ios/vision/image_classifier/utils/sources/MPPImageClassifierResult+Helpers.mm b/mediapipe/tasks/ios/vision/image_classifier/utils/sources/MPPImageClassifierResult+Helpers.mm index 09e21b27..f5199765 100644 --- a/mediapipe/tasks/ios/vision/image_classifier/utils/sources/MPPImageClassifierResult+Helpers.mm +++ b/mediapipe/tasks/ios/vision/image_classifier/utils/sources/MPPImageClassifierResult+Helpers.mm @@ -34,7 +34,7 @@ using ::mediapipe::Packet; return [[MPPImageClassifierResult alloc] initWithClassificationResult:classificationResult - timestampMs:(NSInteger)(packet.Timestamp().Value() / + timestampInMilliseconds:(NSInteger)(packet.Timestamp().Value() / kMicroSecondsPerMilliSecond)]; } diff --git a/mediapipe/tasks/ios/vision/object_detector/sources/MPPObjectDetectionResult.h b/mediapipe/tasks/ios/vision/object_detector/sources/MPPObjectDetectionResult.h index 590867bf..da9899d4 100644 --- a/mediapipe/tasks/ios/vision/object_detector/sources/MPPObjectDetectionResult.h +++ b/mediapipe/tasks/ios/vision/object_detector/sources/MPPObjectDetectionResult.h @@ -36,13 +36,13 @@ NS_SWIFT_NAME(ObjectDetectionResult) * @param detections An array of `MPPDetection` objects each of which has a bounding box that is * expressed in the unrotated input frame of reference coordinates system, i.e. in `[0,image_width) * x [0,image_height)`, which are the dimensions of the underlying image data. - * @param timestampMs The timestamp for this result. + * @param timestampInMilliseconds The timestamp (in milliseconds) for this result. * * @return An instance of `MPPObjectDetectionResult` initialized with the given array of detections * and timestamp (in milliseconds). */ - (instancetype)initWithDetections:(NSArray *)detections - timestampMs:(NSInteger)timestampMs; + timestampInMilliseconds:(NSInteger)timestampInMilliseconds; @end diff --git a/mediapipe/tasks/ios/vision/object_detector/sources/MPPObjectDetectionResult.m b/mediapipe/tasks/ios/vision/object_detector/sources/MPPObjectDetectionResult.m index ac24c19f..18174d07 100644 --- a/mediapipe/tasks/ios/vision/object_detector/sources/MPPObjectDetectionResult.m +++ b/mediapipe/tasks/ios/vision/object_detector/sources/MPPObjectDetectionResult.m @@ -17,8 +17,8 @@ @implementation MPPObjectDetectionResult - (instancetype)initWithDetections:(NSArray *)detections - timestampMs:(NSInteger)timestampMs { - self = [super initWithTimestampMs:timestampMs]; + timestampInMilliseconds:(NSInteger)timestampInMilliseconds { + self = [super initWithTimestampMs:timestampInMilliseconds]; if (self) { _detections = detections; } diff --git a/mediapipe/tasks/ios/vision/object_detector/sources/MPPObjectDetector.h b/mediapipe/tasks/ios/vision/object_detector/sources/MPPObjectDetector.h index 58344d0c..f92c90c5 100644 --- a/mediapipe/tasks/ios/vision/object_detector/sources/MPPObjectDetector.h +++ b/mediapipe/tasks/ios/vision/object_detector/sources/MPPObjectDetector.h @@ -138,8 +138,8 @@ NS_SWIFT_NAME(ObjectDetector) * `MPPRunningModeVideo`. * * @param image The `MPPImage` on which object detection is to be performed. - * @param timestampMs The video frame's timestamp (in milliseconds). The input timestamps must be - * monotonically increasing. + * @param timestampInMilliseconds The video frame's timestamp (in milliseconds). The input + * timestamps must be monotonically increasing. * @param error An optional error parameter populated when there is an error in performing object * detection on the input image. * @@ -149,9 +149,9 @@ NS_SWIFT_NAME(ObjectDetector) * image data. */ - (nullable MPPObjectDetectionResult *)detectInVideoFrame:(MPPImage *)image - timestampMs:(NSInteger)timestampMs + timestampInMilliseconds:(NSInteger)timestampInMilliseconds error:(NSError **)error - NS_SWIFT_NAME(detect(videoFrame:timestampMs:)); + NS_SWIFT_NAME(detect(videoFrame:timestampInMilliseconds:)); /** * Performs object detection on the provided video frame of type `MPPImage` cropped to the @@ -164,8 +164,8 @@ NS_SWIFT_NAME(ObjectDetector) * * @param image A live stream image data of type `MPPImage` on which object detection is to be * performed. - * @param timestampMs The video frame's timestamp (in milliseconds). The input timestamps must be - * monotonically increasing. + * @param timestampInMilliseconds The video frame's timestamp (in milliseconds). The input + * timestamps must be monotonically increasing. * @param roi A `CGRect` specifying the region of interest within the given `MPPImage`, on which * object detection should be performed. * @@ -178,10 +178,10 @@ NS_SWIFT_NAME(ObjectDetector) * image data. */ - (nullable MPPObjectDetectionResult *)detectInVideoFrame:(MPPImage *)image - timestampMs:(NSInteger)timestampMs + timestampInMilliseconds:(NSInteger)timestampInMilliseconds regionOfInterest:(CGRect)roi error:(NSError **)error - NS_SWIFT_NAME(detect(videoFrame:timestampMs:regionOfInterest:)); + NS_SWIFT_NAME(detect(videoFrame:timestampInMilliseconds:regionOfInterest:)); /** * Sends live stream image data of type `MPPImage` to perform object detection using the whole @@ -195,16 +195,17 @@ NS_SWIFT_NAME(ObjectDetector) * * @param image A live stream image data of type `MPPImage` on which object detection is to be * performed. - * @param timestampMs The timestamp (in milliseconds) which indicates when the input image is sent - * to the object detector. The input timestamps must be monotonically increasing. + * @param timestampInMilliseconds The timestamp (in milliseconds) which indicates when the input + * image is sent to the object detector. The input timestamps must be monotonically increasing. * @param error An optional error parameter populated when there is an error in performing object * detection on the input live stream image data. * * @return `YES` if the image was sent to the task successfully, otherwise `NO`. */ - (BOOL)detectAsyncInImage:(MPPImage *)image - timestampMs:(NSInteger)timestampMs - error:(NSError **)error NS_SWIFT_NAME(detectAsync(image:timestampMs:)); + timestampInMilliseconds:(NSInteger)timestampInMilliseconds + error:(NSError **)error + NS_SWIFT_NAME(detectAsync(image:timestampInMilliseconds:)); /** * Sends live stream image data of type `MPPImage` to perform object detection, cropped to the @@ -218,8 +219,8 @@ NS_SWIFT_NAME(ObjectDetector) * * @param image A live stream image data of type `MPPImage` on which object detection is to be * performed. - * @param timestampMs The timestamp (in milliseconds) which indicates when the input image is sent - * to the object detector. The input timestamps must be monotonically increasing. + * @param timestampInMilliseconds The timestamp (in milliseconds) which indicates when the input + * image is sent to the object detector. The input timestamps must be monotonically increasing. * @param roi A `CGRect` specifying the region of interest within the given live stream image data * of type `MPPImage`, on which iobject detection should be performed. * @param error An optional error parameter populated when there is an error in performing object @@ -228,10 +229,10 @@ NS_SWIFT_NAME(ObjectDetector) * @return `YES` if the image was sent to the task successfully, otherwise `NO`. */ - (BOOL)detectAsyncInImage:(MPPImage *)image - timestampMs:(NSInteger)timestampMs - regionOfInterest:(CGRect)roi - error:(NSError **)error - NS_SWIFT_NAME(detectAsync(image:timestampMs:regionOfInterest:)); + timestampInMilliseconds:(NSInteger)timestampInMilliseconds + regionOfInterest:(CGRect)roi + error:(NSError **)error + NS_SWIFT_NAME(detectAsync(image:timestampInMilliseconds:regionOfInterest:)); - (instancetype)init NS_UNAVAILABLE; diff --git a/mediapipe/tasks/ios/vision/object_detector/sources/MPPObjectDetector.mm b/mediapipe/tasks/ios/vision/object_detector/sources/MPPObjectDetector.mm index 53dcad4a..e1aa11e9 100644 --- a/mediapipe/tasks/ios/vision/object_detector/sources/MPPObjectDetector.mm +++ b/mediapipe/tasks/ios/vision/object_detector/sources/MPPObjectDetector.mm @@ -157,7 +157,7 @@ static NSString *const kTaskGraphName = @"mediapipe.tasks.vision.ObjectDetectorG } - (std::optional)inputPacketMapWithMPPImage:(MPPImage *)image - timestampMs:(NSInteger)timestampMs + timestampInMilliseconds:(NSInteger)timestampInMilliseconds regionOfInterest:(CGRect)roi error:(NSError **)error { std::optional rect = @@ -170,14 +170,15 @@ static NSString *const kTaskGraphName = @"mediapipe.tasks.vision.ObjectDetectorG } Packet imagePacket = [MPPVisionPacketCreator createPacketWithMPPImage:image - timestampMs:timestampMs + timestampInMilliseconds:timestampInMilliseconds error:error]; if (imagePacket.IsEmpty()) { return std::nullopt; } - Packet normalizedRectPacket = [MPPVisionPacketCreator createPacketWithNormalizedRect:rect.value() - timestampMs:timestampMs]; + Packet normalizedRectPacket = + [MPPVisionPacketCreator createPacketWithNormalizedRect:rect.value() + timestampInMilliseconds:timestampInMilliseconds]; PacketMap inputPacketMap = InputPacketMap(imagePacket, normalizedRectPacket); return inputPacketMap; @@ -188,11 +189,11 @@ static NSString *const kTaskGraphName = @"mediapipe.tasks.vision.ObjectDetectorG } - (nullable MPPObjectDetectionResult *)detectInVideoFrame:(MPPImage *)image - timestampMs:(NSInteger)timestampMs + timestampInMilliseconds:(NSInteger)timestampInMilliseconds regionOfInterest:(CGRect)roi error:(NSError **)error { std::optional inputPacketMap = [self inputPacketMapWithMPPImage:image - timestampMs:timestampMs + timestampInMilliseconds:timestampInMilliseconds regionOfInterest:roi error:error]; if (!inputPacketMap.has_value()) { @@ -212,20 +213,20 @@ static NSString *const kTaskGraphName = @"mediapipe.tasks.vision.ObjectDetectorG } - (nullable MPPObjectDetectionResult *)detectInVideoFrame:(MPPImage *)image - timestampMs:(NSInteger)timestampMs + timestampInMilliseconds:(NSInteger)timestampInMilliseconds error:(NSError **)error { return [self detectInVideoFrame:image - timestampMs:timestampMs + timestampInMilliseconds:timestampInMilliseconds regionOfInterest:CGRectZero error:error]; } - (BOOL)detectAsyncInImage:(MPPImage *)image - timestampMs:(NSInteger)timestampMs - regionOfInterest:(CGRect)roi - error:(NSError **)error { + timestampInMilliseconds:(NSInteger)timestampInMilliseconds + regionOfInterest:(CGRect)roi + error:(NSError **)error { std::optional inputPacketMap = [self inputPacketMapWithMPPImage:image - timestampMs:timestampMs + timestampInMilliseconds:timestampInMilliseconds regionOfInterest:roi error:error]; if (!inputPacketMap.has_value()) { @@ -236,10 +237,10 @@ static NSString *const kTaskGraphName = @"mediapipe.tasks.vision.ObjectDetectorG } - (BOOL)detectAsyncInImage:(MPPImage *)image - timestampMs:(NSInteger)timestampMs - error:(NSError **)error { + timestampInMilliseconds:(NSInteger)timestampInMilliseconds + error:(NSError **)error { return [self detectAsyncInImage:image - timestampMs:timestampMs + timestampInMilliseconds:timestampInMilliseconds regionOfInterest:CGRectZero error:error]; } diff --git a/mediapipe/tasks/ios/vision/object_detector/utils/sources/MPPObjectDetectionResult+Helpers.mm b/mediapipe/tasks/ios/vision/object_detector/utils/sources/MPPObjectDetectionResult+Helpers.mm index 3507b7d7..225a6993 100644 --- a/mediapipe/tasks/ios/vision/object_detector/utils/sources/MPPObjectDetectionResult+Helpers.mm +++ b/mediapipe/tasks/ios/vision/object_detector/utils/sources/MPPObjectDetectionResult+Helpers.mm @@ -38,8 +38,9 @@ using ::mediapipe::Packet; } return [[MPPObjectDetectionResult alloc] - initWithDetections:detections - timestampMs:(NSInteger)(packet.Timestamp().Value() / kMicroSecondsPerMilliSecond)]; + initWithDetections:detections + timestampInMilliseconds:(NSInteger)(packet.Timestamp().Value() / + kMicroSecondsPerMilliSecond)]; } @end From a03fa448dc09d75da7f5eecae16a1a2909a46df6 Mon Sep 17 00:00:00 2001 From: kinaryml Date: Thu, 13 Apr 2023 11:55:37 -0700 Subject: [PATCH 03/30] Explicitly state the modes in the tests for ImageSegmenterOptions and InteractiveSegmenterOptions --- .../test/vision/image_segmenter_test.py | 32 +++++++++++++------ .../test/vision/interactive_segmenter_test.py | 18 ++++++++--- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/mediapipe/tasks/python/test/vision/image_segmenter_test.py b/mediapipe/tasks/python/test/vision/image_segmenter_test.py index 32792519..aa557281 100644 --- a/mediapipe/tasks/python/test/vision/image_segmenter_test.py +++ b/mediapipe/tasks/python/test/vision/image_segmenter_test.py @@ -157,7 +157,9 @@ class ImageSegmenterTest(parameterized.TestCase): raise ValueError('model_file_type is invalid.') options = _ImageSegmenterOptions( - base_options=base_options, output_category_mask=True) + base_options=base_options, output_category_mask=True, + output_confidence_masks=False + ) segmenter = _ImageSegmenter.create_from_options(options) # Performs image segmentation on the input. @@ -188,8 +190,9 @@ class ImageSegmenterTest(parameterized.TestCase): # Run segmentation on the model in CONFIDENCE_MASK mode. options = _ImageSegmenterOptions( - base_options=base_options, - activation=_Activation.SOFTMAX) + base_options=base_options, output_category_mask=False, + output_confidence_masks=True, activation=_Activation.SOFTMAX + ) with _ImageSegmenter.create_from_options(options) as segmenter: segmentation_result = segmenter.segment(test_image) @@ -279,7 +282,9 @@ class ImageSegmenterTest(parameterized.TestCase): options = _ImageSegmenterOptions( base_options=_BaseOptions(model_asset_path=self.model_path), output_category_mask=True, - running_mode=_RUNNING_MODE.VIDEO) + output_confidence_masks=False, + running_mode=_RUNNING_MODE.VIDEO + ) with _ImageSegmenter.create_from_options(options) as segmenter: for timestamp in range(0, 300, 30): segmentation_result = segmenter.segment_for_video( @@ -297,8 +302,10 @@ class ImageSegmenterTest(parameterized.TestCase): os.path.join(_TEST_DATA_DIR, _CAT_IMAGE))) options = _ImageSegmenterOptions( - base_options=_BaseOptions(model_asset_path=self.model_path), - running_mode=_RUNNING_MODE.VIDEO) + base_options=_BaseOptions(model_asset_path=self.model_path), + running_mode=_RUNNING_MODE.VIDEO, output_category_mask=False, + output_confidence_masks=True + ) with _ImageSegmenter.create_from_options(options) as segmenter: for timestamp in range(0, 300, 30): segmentation_result = segmenter.segment_for_video( @@ -370,8 +377,10 @@ class ImageSegmenterTest(parameterized.TestCase): options = _ImageSegmenterOptions( base_options=_BaseOptions(model_asset_path=self.model_path), output_category_mask=True, + output_confidence_masks=False, running_mode=_RUNNING_MODE.LIVE_STREAM, - result_callback=check_result) + result_callback=check_result + ) with _ImageSegmenter.create_from_options(options) as segmenter: for timestamp in range(0, 300, 30): segmenter.segment_async(self.test_image, timestamp) @@ -405,9 +414,12 @@ class ImageSegmenterTest(parameterized.TestCase): self.observed_timestamp_ms = timestamp_ms options = _ImageSegmenterOptions( - base_options=_BaseOptions(model_asset_path=self.model_path), - running_mode=_RUNNING_MODE.LIVE_STREAM, - result_callback=check_result) + base_options=_BaseOptions(model_asset_path=self.model_path), + running_mode=_RUNNING_MODE.LIVE_STREAM, + output_category_mask=False, + output_confidence_masks=True, + result_callback=check_result + ) with _ImageSegmenter.create_from_options(options) as segmenter: for timestamp in range(0, 300, 30): segmenter.segment_async(test_image, timestamp) diff --git a/mediapipe/tasks/python/test/vision/interactive_segmenter_test.py b/mediapipe/tasks/python/test/vision/interactive_segmenter_test.py index 6af15aa0..aea4f8a1 100644 --- a/mediapipe/tasks/python/test/vision/interactive_segmenter_test.py +++ b/mediapipe/tasks/python/test/vision/interactive_segmenter_test.py @@ -200,7 +200,8 @@ class InteractiveSegmenterTest(parameterized.TestCase): raise ValueError('model_file_type is invalid.') options = _InteractiveSegmenterOptions( - base_options=base_options, output_category_mask=True + base_options=base_options, output_category_mask=True, + output_confidence_masks=False ) segmenter = _InteractiveSegmenter.create_from_options(options) @@ -252,7 +253,10 @@ class InteractiveSegmenterTest(parameterized.TestCase): roi = _RegionOfInterest(format=roi_format, keypoint=keypoint) # Run segmentation on the model in CONFIDENCE_MASK mode. - options = _InteractiveSegmenterOptions(base_options=base_options) + options = _InteractiveSegmenterOptions( + base_options=base_options, output_category_mask=False, + output_confidence_masks=True + ) with _InteractiveSegmenter.create_from_options(options) as segmenter: # Perform segmentation @@ -284,7 +288,10 @@ class InteractiveSegmenterTest(parameterized.TestCase): ) # Run segmentation on the model in CONFIDENCE_MASK mode. - options = _InteractiveSegmenterOptions(base_options=base_options) + options = _InteractiveSegmenterOptions( + base_options=base_options, output_category_mask=False, + output_confidence_masks=True + ) with _InteractiveSegmenter.create_from_options(options) as segmenter: # Perform segmentation @@ -310,7 +317,10 @@ class InteractiveSegmenterTest(parameterized.TestCase): ) # Run segmentation on the model in CONFIDENCE_MASK mode. - options = _InteractiveSegmenterOptions(base_options=base_options) + options = _InteractiveSegmenterOptions( + base_options=base_options, output_category_mask=False, + output_confidence_masks=True + ) with self.assertRaisesRegex( ValueError, "This task doesn't support region-of-interest." From 17f2d0e1e5558dea865d0a59644c1e456be56190 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Thu, 13 Apr 2023 14:19:10 -0600 Subject: [PATCH 04/30] Add Face Landmarks Connection constants for the Python API --- .../tasks/python/vision/face_landmarker.py | 944 ++++++++++++++++++ 1 file changed, 944 insertions(+) diff --git a/mediapipe/tasks/python/vision/face_landmarker.py b/mediapipe/tasks/python/vision/face_landmarker.py index 3e43e8a7..8716a24d 100644 --- a/mediapipe/tasks/python/vision/face_landmarker.py +++ b/mediapipe/tasks/python/vision/face_landmarker.py @@ -120,6 +120,950 @@ class Blendshapes(enum.IntEnum): NOSE_SNEER_RIGHT = 51 +@dataclasses.dataclass +class Connection: + start: int + end: int + + +class FaceLandmarksConnections: + FACE_LANDMARKS_LIPS: List[Connection] = [ + Connection(61, 146), Connection(146, 91), Connection(91, 181), + Connection(181, 84), Connection(84, 17), Connection(17, 314), + Connection(314, 405), Connection(405, 321), Connection(321, 375), + Connection(375, 291), Connection(61, 185), Connection(185, 40), + Connection(40, 39), Connection(39, 37), Connection(37, 0), + Connection(0, 267), Connection(267, 269), Connection(269, 270), + Connection(270, 409), Connection(409, 291), Connection(78, 95), + Connection(95, 88), Connection(88, 178), Connection(178, 87), + Connection(87, 14), Connection(14, 317), Connection(317, 402), + Connection(402, 318), Connection(318, 324), Connection(324, 308), + Connection(78, 191), Connection(191, 80), Connection(80, 81), + Connection(81, 82), Connection(82, 13), Connection(13, 312), + Connection(312, 311), Connection(311, 310), Connection(310, 415), + Connection(415, 308) + ] + + FACE_LANDMARKS_LEFT_EYE: List[Connection] = [ + Connection(263, 249), Connection(249, 390), Connection(390, 373), + Connection(373, 374), Connection(374, 380), Connection(380, 381), + Connection(381, 382), Connection(382, 362), Connection(263, 466), + Connection(466, 388), Connection(388, 387), Connection(387, 386), + Connection(386, 385), Connection(385, 384), Connection(384, 398), + Connection(398, 362) + ] + + FACE_LANDMARKS_LEFT_EYEBROW: List[Connection] = [ + Connection(276, 283), Connection(283, 282), Connection(282, 295), + Connection(295, 285), Connection(300, 293), Connection(293, 334), + Connection(334, 296), Connection(296, 336) + ] + + FACE_LANDMARKS_LEFT_IRIS: List[Connection] = [ + Connection(474, 475), Connection(475, 476), Connection(476, 477), + Connection(477, 474) + ] + + FACE_LANDMARKS_RIGHT_EYE: List[Connection] = [ + Connection(33, 7), Connection(7, 163), Connection(163, 144), + Connection(144, 145), Connection(145, 153), Connection(153, 154), + Connection(154, 155), Connection(155, 133), Connection(33, 246), + Connection(246, 161), Connection(161, 160), Connection(160, 159), + Connection(159, 158), Connection(158, 157), Connection(157, 173), + Connection(173, 133) + ] + + FACE_LANDMARKS_RIGHT_EYEBROW: List[Connection] = [ + Connection(46, 53), Connection(53, 52), Connection(52, 65), + Connection(65, 55), Connection(70, 63), Connection(63, 105), + Connection(105, 66), Connection(66, 107) + ] + + FACE_LANDMARKS_RIGHT_IRIS: List[Connection] = [ + Connection(469, 470), Connection(470, 471), Connection(471, 472), + Connection(472, 469) + ] + + FACE_LANDMARKS_FACE_OVAL: List[Connection] = [ + Connection(10, 338), Connection(338, 297), Connection(297, 332), + Connection(332, 284), Connection(284, 251), Connection(251, 389), + Connection(389, 356), Connection(356, 454), Connection(454, 323), + Connection(323, 361), Connection(361, 288), Connection(288, 397), + Connection(397, 365), Connection(365, 379), Connection(379, 378), + Connection(378, 400), Connection(400, 377), Connection(377, 152), + Connection(152, 148), Connection(148, 176), Connection(176, 149), + Connection(149, 150), Connection(150, 136), Connection(136, 172), + Connection(172, 58), Connection(58, 132), Connection(132, 93), + Connection(93, 234), Connection(234, 127), Connection(127, 162), + Connection(162, 21), Connection(21, 54), Connection(54, 103), + Connection(103, 67), Connection(67, 109), Connection(109, 10) + ] + + FACE_LANDMARKS_CONTOURS: List[Connection] = ( + FACE_LANDMARKS_LIPS + + FACE_LANDMARKS_LEFT_EYE + + FACE_LANDMARKS_LEFT_EYEBROW + + FACE_LANDMARKS_RIGHT_EYE + + FACE_LANDMARKS_RIGHT_EYEBROW + + FACE_LANDMARKS_FACE_OVAL + ) + + FACE_LANDMARKS_TESSELATION: List[Connection] = [ + Connection(127, 34), Connection(34, 139), Connection(139, 127), + Connection(11, 0), Connection(0, 37), Connection(37, 11), + Connection(232, 231), Connection(231, 120), Connection(120, 232), + Connection(72, 37), Connection(37, 39), Connection(39, 72), + Connection(128, 121), Connection(121, 47), Connection(47, 128), + Connection(232, 121), Connection(121, 128), Connection(128, 232), + Connection(104, 69), Connection(69, 67), Connection(67, 104), + Connection(175, 171), Connection(171, 148), Connection(148, 175), + Connection(118, 50), Connection(50, 101), Connection(101, 118), + Connection(73, 39), Connection(39, 40), Connection(40, 73), + Connection(9, 151), Connection(151, 108), Connection(108, 9), + Connection(48, 115), Connection(115, 131), Connection(131, 48), + Connection(194, 204), Connection(204, 211), Connection(211, 194), + Connection(74, 40), Connection(40, 185), Connection(185, 74), + Connection(80, 42), Connection(42, 183), Connection(183, 80), + Connection(40, 92), Connection(92, 186), Connection(186, 40), + Connection(230, 229), Connection(229, 118), Connection(118, 230), + Connection(202, 212), Connection(212, 214), Connection(214, 202), + Connection(83, 18), Connection(18, 17), Connection(17, 83), + Connection(76, 61), Connection(61, 146), Connection(146, 76), + Connection(160, 29), Connection(29, 30), Connection(30, 160), + Connection(56, 157), Connection(157, 173), Connection(173, 56), + Connection(106, 204), Connection(204, 194), Connection(194, 106), + Connection(135, 214), Connection(214, 192), Connection(192, 135), + Connection(203, 165), Connection(165, 98), Connection(98, 203), + Connection(21, 71), Connection(71, 68), Connection(68, 21), + Connection(51, 45), Connection(45, 4), Connection(4, 51), + Connection(144, 24), Connection(24, 23), Connection(23, 144), + Connection(77, 146), Connection(146, 91), Connection(91, 77), + Connection(205, 50), Connection(50, 187), Connection(187, 205), + Connection(201, 200), Connection(200, 18), Connection(18, 201), + Connection(91, 106), Connection(106, 182), Connection(182, 91), + Connection(90, 91), Connection(91, 181), Connection(181, 90), + Connection(85, 84), Connection(84, 17), Connection(17, 85), + Connection(206, 203), Connection(203, 36), Connection(36, 206), + Connection(148, 171), Connection(171, 140), Connection(140, 148), + Connection(92, 40), Connection(40, 39), Connection(39, 92), + Connection(193, 189), Connection(189, 244), Connection(244, 193), + Connection(159, 158), Connection(158, 28), Connection(28, 159), + Connection(247, 246), Connection(246, 161), Connection(161, 247), + Connection(236, 3), Connection(3, 196), Connection(196, 236), + Connection(54, 68), Connection(68, 104), Connection(104, 54), + Connection(193, 168), Connection(168, 8), Connection(8, 193), + Connection(117, 228), Connection(228, 31), Connection(31, 117), + Connection(189, 193), Connection(193, 55), Connection(55, 189), + Connection(98, 97), Connection(97, 99), Connection(99, 98), + Connection(126, 47), Connection(47, 100), Connection(100, 126), + Connection(166, 79), Connection(79, 218), Connection(218, 166), + Connection(155, 154), Connection(154, 26), Connection(26, 155), + Connection(209, 49), Connection(49, 131), Connection(131, 209), + Connection(135, 136), Connection(136, 150), Connection(150, 135), + Connection(47, 126), Connection(126, 217), Connection(217, 47), + Connection(223, 52), Connection(52, 53), Connection(53, 223), + Connection(45, 51), Connection(51, 134), Connection(134, 45), + Connection(211, 170), Connection(170, 140), Connection(140, 211), + Connection(67, 69), Connection(69, 108), Connection(108, 67), + Connection(43, 106), Connection(106, 91), Connection(91, 43), + Connection(230, 119), Connection(119, 120), Connection(120, 230), + Connection(226, 130), Connection(130, 247), Connection(247, 226), + Connection(63, 53), Connection(53, 52), Connection(52, 63), + Connection(238, 20), Connection(20, 242), Connection(242, 238), + Connection(46, 70), Connection(70, 156), Connection(156, 46), + Connection(78, 62), Connection(62, 96), Connection(96, 78), + Connection(46, 53), Connection(53, 63), Connection(63, 46), + Connection(143, 34), Connection(34, 227), Connection(227, 143), + Connection(123, 117), Connection(117, 111), Connection(111, 123), + Connection(44, 125), Connection(125, 19), Connection(19, 44), + Connection(236, 134), Connection(134, 51), Connection(51, 236), + Connection(216, 206), Connection(206, 205), Connection(205, 216), + Connection(154, 153), Connection(153, 22), Connection(22, 154), + Connection(39, 37), Connection(37, 167), Connection(167, 39), + Connection(200, 201), Connection(201, 208), Connection(208, 200), + Connection(36, 142), Connection(142, 100), Connection(100, 36), + Connection(57, 212), Connection(212, 202), Connection(202, 57), + Connection(20, 60), Connection(60, 99), Connection(99, 20), + Connection(28, 158), Connection(158, 157), Connection(157, 28), + Connection(35, 226), Connection(226, 113), Connection(113, 35), + Connection(160, 159), Connection(159, 27), Connection(27, 160), + Connection(204, 202), Connection(202, 210), Connection(210, 204), + Connection(113, 225), Connection(225, 46), Connection(46, 113), + Connection(43, 202), Connection(202, 204), Connection(204, 43), + Connection(62, 76), Connection(76, 77), Connection(77, 62), + Connection(137, 123), Connection(123, 116), Connection(116, 137), + Connection(41, 38), Connection(38, 72), Connection(72, 41), + Connection(203, 129), Connection(129, 142), Connection(142, 203), + Connection(64, 98), Connection(98, 240), Connection(240, 64), + Connection(49, 102), Connection(102, 64), Connection(64, 49), + Connection(41, 73), Connection(73, 74), Connection(74, 41), + Connection(212, 216), Connection(216, 207), Connection(207, 212), + Connection(42, 74), Connection(74, 184), Connection(184, 42), + Connection(169, 170), Connection(170, 211), Connection(211, 169), + Connection(170, 149), Connection(149, 176), Connection(176, 170), + Connection(105, 66), Connection(66, 69), Connection(69, 105), + Connection(122, 6), Connection(6, 168), Connection(168, 122), + Connection(123, 147), Connection(147, 187), Connection(187, 123), + Connection(96, 77), Connection(77, 90), Connection(90, 96), + Connection(65, 55), Connection(55, 107), Connection(107, 65), + Connection(89, 90), Connection(90, 180), Connection(180, 89), + Connection(101, 100), Connection(100, 120), Connection(120, 101), + Connection(63, 105), Connection(105, 104), Connection(104, 63), + Connection(93, 137), Connection(137, 227), Connection(227, 93), + Connection(15, 86), Connection(86, 85), Connection(85, 15), + Connection(129, 102), Connection(102, 49), Connection(49, 129), + Connection(14, 87), Connection(87, 86), Connection(86, 14), + Connection(55, 8), Connection(8, 9), Connection(9, 55), + Connection(100, 47), Connection(47, 121), Connection(121, 100), + Connection(145, 23), Connection(23, 22), Connection(22, 145), + Connection(88, 89), Connection(89, 179), Connection(179, 88), + Connection(6, 122), Connection(122, 196), Connection(196, 6), + Connection(88, 95), Connection(95, 96), Connection(96, 88), + Connection(138, 172), Connection(172, 136), Connection(136, 138), + Connection(215, 58), Connection(58, 172), Connection(172, 215), + Connection(115, 48), Connection(48, 219), Connection(219, 115), + Connection(42, 80), Connection(80, 81), Connection(81, 42), + Connection(195, 3), Connection(3, 51), Connection(51, 195), + Connection(43, 146), Connection(146, 61), Connection(61, 43), + Connection(171, 175), Connection(175, 199), Connection(199, 171), + Connection(81, 82), Connection(82, 38), Connection(38, 81), + Connection(53, 46), Connection(46, 225), Connection(225, 53), + Connection(144, 163), Connection(163, 110), Connection(110, 144), + Connection(52, 65), Connection(65, 66), Connection(66, 52), + Connection(229, 228), Connection(228, 117), Connection(117, 229), + Connection(34, 127), Connection(127, 234), Connection(234, 34), + Connection(107, 108), Connection(108, 69), Connection(69, 107), + Connection(109, 108), Connection(108, 151), Connection(151, 109), + Connection(48, 64), Connection(64, 235), Connection(235, 48), + Connection(62, 78), Connection(78, 191), Connection(191, 62), + Connection(129, 209), Connection(209, 126), Connection(126, 129), + Connection(111, 35), Connection(35, 143), Connection(143, 111), + Connection(117, 123), Connection(123, 50), Connection(50, 117), + Connection(222, 65), Connection(65, 52), Connection(52, 222), + Connection(19, 125), Connection(125, 141), Connection(141, 19), + Connection(221, 55), Connection(55, 65), Connection(65, 221), + Connection(3, 195), Connection(195, 197), Connection(197, 3), + Connection(25, 7), Connection(7, 33), Connection(33, 25), + Connection(220, 237), Connection(237, 44), Connection(44, 220), + Connection(70, 71), Connection(71, 139), Connection(139, 70), + Connection(122, 193), Connection(193, 245), Connection(245, 122), + Connection(247, 130), Connection(130, 33), Connection(33, 247), + Connection(71, 21), Connection(21, 162), Connection(162, 71), + Connection(170, 169), Connection(169, 150), Connection(150, 170), + Connection(188, 174), Connection(174, 196), Connection(196, 188), + Connection(216, 186), Connection(186, 92), Connection(92, 216), + Connection(2, 97), Connection(97, 167), Connection(167, 2), + Connection(141, 125), Connection(125, 241), Connection(241, 141), + Connection(164, 167), Connection(167, 37), Connection(37, 164), + Connection(72, 38), Connection(38, 12), Connection(12, 72), + Connection(38, 82), Connection(82, 13), Connection(13, 38), + Connection(63, 68), Connection(68, 71), Connection(71, 63), + Connection(226, 35), Connection(35, 111), Connection(111, 226), + Connection(101, 50), Connection(50, 205), Connection(205, 101), + Connection(206, 92), Connection(92, 165), Connection(165, 206), + Connection(209, 198), Connection(198, 217), Connection(217, 209), + Connection(165, 167), Connection(167, 97), Connection(97, 165), + Connection(220, 115), Connection(115, 218), Connection(218, 220), + Connection(133, 112), Connection(112, 243), Connection(243, 133), + Connection(239, 238), Connection(238, 241), Connection(241, 239), + Connection(214, 135), Connection(135, 169), Connection(169, 214), + Connection(190, 173), Connection(173, 133), Connection(133, 190), + Connection(171, 208), Connection(208, 32), Connection(32, 171), + Connection(125, 44), Connection(44, 237), Connection(237, 125), + Connection(86, 87), Connection(87, 178), Connection(178, 86), + Connection(85, 86), Connection(86, 179), Connection(179, 85), + Connection(84, 85), Connection(85, 180), Connection(180, 84), + Connection(83, 84), Connection(84, 181), Connection(181, 83), + Connection(201, 83), Connection(83, 182), Connection(182, 201), + Connection(137, 93), Connection(93, 132), Connection(132, 137), + Connection(76, 62), Connection(62, 183), Connection(183, 76), + Connection(61, 76), Connection(76, 184), Connection(184, 61), + Connection(57, 61), Connection(61, 185), Connection(185, 57), + Connection(212, 57), Connection(57, 186), Connection(186, 212), + Connection(214, 207), Connection(207, 187), Connection(187, 214), + Connection(34, 143), Connection(143, 156), Connection(156, 34), + Connection(79, 239), Connection(239, 237), Connection(237, 79), + Connection(123, 137), Connection(137, 177), Connection(177, 123), + Connection(44, 1), Connection(1, 4), Connection(4, 44), + Connection(201, 194), Connection(194, 32), Connection(32, 201), + Connection(64, 102), Connection(102, 129), Connection(129, 64), + Connection(213, 215), Connection(215, 138), Connection(138, 213), + Connection(59, 166), Connection(166, 219), Connection(219, 59), + Connection(242, 99), Connection(99, 97), Connection(97, 242), + Connection(2, 94), Connection(94, 141), Connection(141, 2), + Connection(75, 59), Connection(59, 235), Connection(235, 75), + Connection(24, 110), Connection(110, 228), Connection(228, 24), + Connection(25, 130), Connection(130, 226), Connection(226, 25), + Connection(23, 24), Connection(24, 229), Connection(229, 23), + Connection(22, 23), Connection(23, 230), Connection(230, 22), + Connection(26, 22), Connection(22, 231), Connection(231, 26), + Connection(112, 26), Connection(26, 232), Connection(232, 112), + Connection(189, 190), Connection(190, 243), Connection(243, 189), + Connection(221, 56), Connection(56, 190), Connection(190, 221), + Connection(28, 56), Connection(56, 221), Connection(221, 28), + Connection(27, 28), Connection(28, 222), Connection(222, 27), + Connection(29, 27), Connection(27, 223), Connection(223, 29), + Connection(30, 29), Connection(29, 224), Connection(224, 30), + Connection(247, 30), Connection(30, 225), Connection(225, 247), + Connection(238, 79), Connection(79, 20), Connection(20, 238), + Connection(166, 59), Connection(59, 75), Connection(75, 166), + Connection(60, 75), Connection(75, 240), Connection(240, 60), + Connection(147, 177), Connection(177, 215), Connection(215, 147), + Connection(20, 79), Connection(79, 166), Connection(166, 20), + Connection(187, 147), Connection(147, 213), Connection(213, 187), + Connection(112, 233), Connection(233, 244), Connection(244, 112), + Connection(233, 128), Connection(128, 245), Connection(245, 233), + Connection(128, 114), Connection(114, 188), Connection(188, 128), + Connection(114, 217), Connection(217, 174), Connection(174, 114), + Connection(131, 115), Connection(115, 220), Connection(220, 131), + Connection(217, 198), Connection(198, 236), Connection(236, 217), + Connection(198, 131), Connection(131, 134), Connection(134, 198), + Connection(177, 132), Connection(132, 58), Connection(58, 177), + Connection(143, 35), Connection(35, 124), Connection(124, 143), + Connection(110, 163), Connection(163, 7), Connection(7, 110), + Connection(228, 110), Connection(110, 25), Connection(25, 228), + Connection(356, 389), Connection(389, 368), Connection(368, 356), + Connection(11, 302), Connection(302, 267), Connection(267, 11), + Connection(452, 350), Connection(350, 349), Connection(349, 452), + Connection(302, 303), Connection(303, 269), Connection(269, 302), + Connection(357, 343), Connection(343, 277), Connection(277, 357), + Connection(452, 453), Connection(453, 357), Connection(357, 452), + Connection(333, 332), Connection(332, 297), Connection(297, 333), + Connection(175, 152), Connection(152, 377), Connection(377, 175), + Connection(347, 348), Connection(348, 330), Connection(330, 347), + Connection(303, 304), Connection(304, 270), Connection(270, 303), + Connection(9, 336), Connection(336, 337), Connection(337, 9), + Connection(278, 279), Connection(279, 360), Connection(360, 278), + Connection(418, 262), Connection(262, 431), Connection(431, 418), + Connection(304, 408), Connection(408, 409), Connection(409, 304), + Connection(310, 415), Connection(415, 407), Connection(407, 310), + Connection(270, 409), Connection(409, 410), Connection(410, 270), + Connection(450, 348), Connection(348, 347), Connection(347, 450), + Connection(422, 430), Connection(430, 434), Connection(434, 422), + Connection(313, 314), Connection(314, 17), Connection(17, 313), + Connection(306, 307), Connection(307, 375), Connection(375, 306), + Connection(387, 388), Connection(388, 260), Connection(260, 387), + Connection(286, 414), Connection(414, 398), Connection(398, 286), + Connection(335, 406), Connection(406, 418), Connection(418, 335), + Connection(364, 367), Connection(367, 416), Connection(416, 364), + Connection(423, 358), Connection(358, 327), Connection(327, 423), + Connection(251, 284), Connection(284, 298), Connection(298, 251), + Connection(281, 5), Connection(5, 4), Connection(4, 281), + Connection(373, 374), Connection(374, 253), Connection(253, 373), + Connection(307, 320), Connection(320, 321), Connection(321, 307), + Connection(425, 427), Connection(427, 411), Connection(411, 425), + Connection(421, 313), Connection(313, 18), Connection(18, 421), + Connection(321, 405), Connection(405, 406), Connection(406, 321), + Connection(320, 404), Connection(404, 405), Connection(405, 320), + Connection(315, 16), Connection(16, 17), Connection(17, 315), + Connection(426, 425), Connection(425, 266), Connection(266, 426), + Connection(377, 400), Connection(400, 369), Connection(369, 377), + Connection(322, 391), Connection(391, 269), Connection(269, 322), + Connection(417, 465), Connection(465, 464), Connection(464, 417), + Connection(386, 257), Connection(257, 258), Connection(258, 386), + Connection(466, 260), Connection(260, 388), Connection(388, 466), + Connection(456, 399), Connection(399, 419), Connection(419, 456), + Connection(284, 332), Connection(332, 333), Connection(333, 284), + Connection(417, 285), Connection(285, 8), Connection(8, 417), + Connection(346, 340), Connection(340, 261), Connection(261, 346), + Connection(413, 441), Connection(441, 285), Connection(285, 413), + Connection(327, 460), Connection(460, 328), Connection(328, 327), + Connection(355, 371), Connection(371, 329), Connection(329, 355), + Connection(392, 439), Connection(439, 438), Connection(438, 392), + Connection(382, 341), Connection(341, 256), Connection(256, 382), + Connection(429, 420), Connection(420, 360), Connection(360, 429), + Connection(364, 394), Connection(394, 379), Connection(379, 364), + Connection(277, 343), Connection(343, 437), Connection(437, 277), + Connection(443, 444), Connection(444, 283), Connection(283, 443), + Connection(275, 440), Connection(440, 363), Connection(363, 275), + Connection(431, 262), Connection(262, 369), Connection(369, 431), + Connection(297, 338), Connection(338, 337), Connection(337, 297), + Connection(273, 375), Connection(375, 321), Connection(321, 273), + Connection(450, 451), Connection(451, 349), Connection(349, 450), + Connection(446, 342), Connection(342, 467), Connection(467, 446), + Connection(293, 334), Connection(334, 282), Connection(282, 293), + Connection(458, 461), Connection(461, 462), Connection(462, 458), + Connection(276, 353), Connection(353, 383), Connection(383, 276), + Connection(308, 324), Connection(324, 325), Connection(325, 308), + Connection(276, 300), Connection(300, 293), Connection(293, 276), + Connection(372, 345), Connection(345, 447), Connection(447, 372), + Connection(352, 345), Connection(345, 340), Connection(340, 352), + Connection(274, 1), Connection(1, 19), Connection(19, 274), + Connection(456, 248), Connection(248, 281), Connection(281, 456), + Connection(436, 427), Connection(427, 425), Connection(425, 436), + Connection(381, 256), Connection(256, 252), Connection(252, 381), + Connection(269, 391), Connection(391, 393), Connection(393, 269), + Connection(200, 199), Connection(199, 428), Connection(428, 200), + Connection(266, 330), Connection(330, 329), Connection(329, 266), + Connection(287, 273), Connection(273, 422), Connection(422, 287), + Connection(250, 462), Connection(462, 328), Connection(328, 250), + Connection(258, 286), Connection(286, 384), Connection(384, 258), + Connection(265, 353), Connection(353, 342), Connection(342, 265), + Connection(387, 259), Connection(259, 257), Connection(257, 387), + Connection(424, 431), Connection(431, 430), Connection(430, 424), + Connection(342, 353), Connection(353, 276), Connection(276, 342), + Connection(273, 335), Connection(335, 424), Connection(424, 273), + Connection(292, 325), Connection(325, 307), Connection(307, 292), + Connection(366, 447), Connection(447, 345), Connection(345, 366), + Connection(271, 303), Connection(303, 302), Connection(302, 271), + Connection(423, 266), Connection(266, 371), Connection(371, 423), + Connection(294, 455), Connection(455, 460), Connection(460, 294), + Connection(279, 278), Connection(278, 294), Connection(294, 279), + Connection(271, 272), Connection(272, 304), Connection(304, 271), + Connection(432, 434), Connection(434, 427), Connection(427, 432), + Connection(272, 407), Connection(407, 408), Connection(408, 272), + Connection(394, 430), Connection(430, 431), Connection(431, 394), + Connection(395, 369), Connection(369, 400), Connection(400, 395), + Connection(334, 333), Connection(333, 299), Connection(299, 334), + Connection(351, 417), Connection(417, 168), Connection(168, 351), + Connection(352, 280), Connection(280, 411), Connection(411, 352), + Connection(325, 319), Connection(319, 320), Connection(320, 325), + Connection(295, 296), Connection(296, 336), Connection(336, 295), + Connection(319, 403), Connection(403, 404), Connection(404, 319), + Connection(330, 348), Connection(348, 349), Connection(349, 330), + Connection(293, 298), Connection(298, 333), Connection(333, 293), + Connection(323, 454), Connection(454, 447), Connection(447, 323), + Connection(15, 16), Connection(16, 315), Connection(315, 15), + Connection(358, 429), Connection(429, 279), Connection(279, 358), + Connection(14, 15), Connection(15, 316), Connection(316, 14), + Connection(285, 336), Connection(336, 9), Connection(9, 285), + Connection(329, 349), Connection(349, 350), Connection(350, 329), + Connection(374, 380), Connection(380, 252), Connection(252, 374), + Connection(318, 402), Connection(402, 403), Connection(403, 318), + Connection(6, 197), Connection(197, 419), Connection(419, 6), + Connection(318, 319), Connection(319, 325), Connection(325, 318), + Connection(367, 364), Connection(364, 365), Connection(365, 367), + Connection(435, 367), Connection(367, 397), Connection(397, 435), + Connection(344, 438), Connection(438, 439), Connection(439, 344), + Connection(272, 271), Connection(271, 311), Connection(311, 272), + Connection(195, 5), Connection(5, 281), Connection(281, 195), + Connection(273, 287), Connection(287, 291), Connection(291, 273), + Connection(396, 428), Connection(428, 199), Connection(199, 396), + Connection(311, 271), Connection(271, 268), Connection(268, 311), + Connection(283, 444), Connection(444, 445), Connection(445, 283), + Connection(373, 254), Connection(254, 339), Connection(339, 373), + Connection(282, 334), Connection(334, 296), Connection(296, 282), + Connection(449, 347), Connection(347, 346), Connection(346, 449), + Connection(264, 447), Connection(447, 454), Connection(454, 264), + Connection(336, 296), Connection(296, 299), Connection(299, 336), + Connection(338, 10), Connection(10, 151), Connection(151, 338), + Connection(278, 439), Connection(439, 455), Connection(455, 278), + Connection(292, 407), Connection(407, 415), Connection(415, 292), + Connection(358, 371), Connection(371, 355), Connection(355, 358), + Connection(340, 345), Connection(345, 372), Connection(372, 340), + Connection(346, 347), Connection(347, 280), Connection(280, 346), + Connection(442, 443), Connection(443, 282), Connection(282, 442), + Connection(19, 94), Connection(94, 370), Connection(370, 19), + Connection(441, 442), Connection(442, 295), Connection(295, 441), + Connection(248, 419), Connection(419, 197), Connection(197, 248), + Connection(263, 255), Connection(255, 359), Connection(359, 263), + Connection(440, 275), Connection(275, 274), Connection(274, 440), + Connection(300, 383), Connection(383, 368), Connection(368, 300), + Connection(351, 412), Connection(412, 465), Connection(465, 351), + Connection(263, 467), Connection(467, 466), Connection(466, 263), + Connection(301, 368), Connection(368, 389), Connection(389, 301), + Connection(395, 378), Connection(378, 379), Connection(379, 395), + Connection(412, 351), Connection(351, 419), Connection(419, 412), + Connection(436, 426), Connection(426, 322), Connection(322, 436), + Connection(2, 164), Connection(164, 393), Connection(393, 2), + Connection(370, 462), Connection(462, 461), Connection(461, 370), + Connection(164, 0), Connection(0, 267), Connection(267, 164), + Connection(302, 11), Connection(11, 12), Connection(12, 302), + Connection(268, 12), Connection(12, 13), Connection(13, 268), + Connection(293, 300), Connection(300, 301), Connection(301, 293), + Connection(446, 261), Connection(261, 340), Connection(340, 446), + Connection(330, 266), Connection(266, 425), Connection(425, 330), + Connection(426, 423), Connection(423, 391), Connection(391, 426), + Connection(429, 355), Connection(355, 437), Connection(437, 429), + Connection(391, 327), Connection(327, 326), Connection(326, 391), + Connection(440, 457), Connection(457, 438), Connection(438, 440), + Connection(341, 382), Connection(382, 362), Connection(362, 341), + Connection(459, 457), Connection(457, 461), Connection(461, 459), + Connection(434, 430), Connection(430, 394), Connection(394, 434), + Connection(414, 463), Connection(463, 362), Connection(362, 414), + Connection(396, 369), Connection(369, 262), Connection(262, 396), + Connection(354, 461), Connection(461, 457), Connection(457, 354), + Connection(316, 403), Connection(403, 402), Connection(402, 316), + Connection(315, 404), Connection(404, 403), Connection(403, 315), + Connection(314, 405), Connection(405, 404), Connection(404, 314), + Connection(313, 406), Connection(406, 405), Connection(405, 313), + Connection(421, 418), Connection(418, 406), Connection(406, 421), + Connection(366, 401), Connection(401, 361), Connection(361, 366), + Connection(306, 408), Connection(408, 407), Connection(407, 306), + Connection(291, 409), Connection(409, 408), Connection(408, 291), + Connection(287, 410), Connection(410, 409), Connection(409, 287), + Connection(432, 436), Connection(436, 410), Connection(410, 432), + Connection(434, 416), Connection(416, 411), Connection(411, 434), + Connection(264, 368), Connection(368, 383), Connection(383, 264), + Connection(309, 438), Connection(438, 457), Connection(457, 309), + Connection(352, 376), Connection(376, 401), Connection(401, 352), + Connection(274, 275), Connection(275, 4), Connection(4, 274), + Connection(421, 428), Connection(428, 262), Connection(262, 421), + Connection(294, 327), Connection(327, 358), Connection(358, 294), + Connection(433, 416), Connection(416, 367), Connection(367, 433), + Connection(289, 455), Connection(455, 439), Connection(439, 289), + Connection(462, 370), Connection(370, 326), Connection(326, 462), + Connection(2, 326), Connection(326, 370), Connection(370, 2), + Connection(305, 460), Connection(460, 455), Connection(455, 305), + Connection(254, 449), Connection(449, 448), Connection(448, 254), + Connection(255, 261), Connection(261, 446), Connection(446, 255), + Connection(253, 450), Connection(450, 449), Connection(449, 253), + Connection(252, 451), Connection(451, 450), Connection(450, 252), + Connection(256, 452), Connection(452, 451), Connection(451, 256), + Connection(341, 453), Connection(453, 452), Connection(452, 341), + Connection(413, 464), Connection(464, 463), Connection(463, 413), + Connection(441, 413), Connection(413, 414), Connection(414, 441), + Connection(258, 442), Connection(442, 441), Connection(441, 258), + Connection(257, 443), Connection(443, 442), Connection(442, 257), + Connection(259, 444), Connection(444, 443), Connection(443, 259), + Connection(260, 445), Connection(445, 444), Connection(444, 260), + Connection(467, 342), Connection(342, 445), Connection(445, 467), + Connection(459, 458), Connection(458, 250), Connection(250, 459), + Connection(289, 392), Connection(392, 290), Connection(290, 289), + Connection(290, 328), Connection(328, 460), Connection(460, 290), + Connection(376, 433), Connection(433, 435), Connection(435, 376), + Connection(250, 290), Connection(290, 392), Connection(392, 250), + Connection(411, 416), Connection(416, 433), Connection(433, 411), + Connection(341, 463), Connection(463, 464), Connection(464, 341), + Connection(453, 464), Connection(464, 465), Connection(465, 453), + Connection(357, 465), Connection(465, 412), Connection(412, 357), + Connection(343, 412), Connection(412, 399), Connection(399, 343), + Connection(360, 363), Connection(363, 440), Connection(440, 360), + Connection(437, 399), Connection(399, 456), Connection(456, 437), + Connection(420, 456), Connection(456, 363), Connection(363, 420), + Connection(401, 435), Connection(435, 288), Connection(288, 401), + Connection(372, 383), Connection(383, 353), Connection(353, 372), + Connection(339, 255), Connection(255, 249), Connection(249, 339), + Connection(448, 261), Connection(261, 255), Connection(255, 448), + Connection(133, 243), Connection(243, 190), Connection(190, 133), + Connection(133, 155), Connection(155, 112), Connection(112, 133), + Connection(33, 246), Connection(246, 247), Connection(247, 33), + Connection(33, 130), Connection(130, 25), Connection(25, 33), + Connection(398, 384), Connection(384, 286), Connection(286, 398), + Connection(362, 398), Connection(398, 414), Connection(414, 362), + Connection(362, 463), Connection(463, 341), Connection(341, 362), + Connection(263, 359), Connection(359, 467), Connection(467, 263), + Connection(263, 249), Connection(249, 255), Connection(255, 263), + Connection(466, 467), Connection(467, 260), Connection(260, 466), + Connection(75, 60), Connection(60, 166), Connection(166, 75), + Connection(238, 239), Connection(239, 79), Connection(79, 238), + Connection(162, 127), Connection(127, 139), Connection(139, 162), + Connection(72, 11), Connection(11, 37), Connection(37, 72), + Connection(121, 232), Connection(232, 120), Connection(120, 121), + Connection(73, 72), Connection(72, 39), Connection(39, 73), + Connection(114, 128), Connection(128, 47), Connection(47, 114), + Connection(233, 232), Connection(232, 128), Connection(128, 233), + Connection(103, 104), Connection(104, 67), Connection(67, 103), + Connection(152, 175), Connection(175, 148), Connection(148, 152), + Connection(119, 118), Connection(118, 101), Connection(101, 119), + Connection(74, 73), Connection(73, 40), Connection(40, 74), + Connection(107, 9), Connection(9, 108), Connection(108, 107), + Connection(49, 48), Connection(48, 131), Connection(131, 49), + Connection(32, 194), Connection(194, 211), Connection(211, 32), + Connection(184, 74), Connection(74, 185), Connection(185, 184), + Connection(191, 80), Connection(80, 183), Connection(183, 191), + Connection(185, 40), Connection(40, 186), Connection(186, 185), + Connection(119, 230), Connection(230, 118), Connection(118, 119), + Connection(210, 202), Connection(202, 214), Connection(214, 210), + Connection(84, 83), Connection(83, 17), Connection(17, 84), + Connection(77, 76), Connection(76, 146), Connection(146, 77), + Connection(161, 160), Connection(160, 30), Connection(30, 161), + Connection(190, 56), Connection(56, 173), Connection(173, 190), + Connection(182, 106), Connection(106, 194), Connection(194, 182), + Connection(138, 135), Connection(135, 192), Connection(192, 138), + Connection(129, 203), Connection(203, 98), Connection(98, 129), + Connection(54, 21), Connection(21, 68), Connection(68, 54), + Connection(5, 51), Connection(51, 4), Connection(4, 5), + Connection(145, 144), Connection(144, 23), Connection(23, 145), + Connection(90, 77), Connection(77, 91), Connection(91, 90), + Connection(207, 205), Connection(205, 187), Connection(187, 207), + Connection(83, 201), Connection(201, 18), Connection(18, 83), + Connection(181, 91), Connection(91, 182), Connection(182, 181), + Connection(180, 90), Connection(90, 181), Connection(181, 180), + Connection(16, 85), Connection(85, 17), Connection(17, 16), + Connection(205, 206), Connection(206, 36), Connection(36, 205), + Connection(176, 148), Connection(148, 140), Connection(140, 176), + Connection(165, 92), Connection(92, 39), Connection(39, 165), + Connection(245, 193), Connection(193, 244), Connection(244, 245), + Connection(27, 159), Connection(159, 28), Connection(28, 27), + Connection(30, 247), Connection(247, 161), Connection(161, 30), + Connection(174, 236), Connection(236, 196), Connection(196, 174), + Connection(103, 54), Connection(54, 104), Connection(104, 103), + Connection(55, 193), Connection(193, 8), Connection(8, 55), + Connection(111, 117), Connection(117, 31), Connection(31, 111), + Connection(221, 189), Connection(189, 55), Connection(55, 221), + Connection(240, 98), Connection(98, 99), Connection(99, 240), + Connection(142, 126), Connection(126, 100), Connection(100, 142), + Connection(219, 166), Connection(166, 218), Connection(218, 219), + Connection(112, 155), Connection(155, 26), Connection(26, 112), + Connection(198, 209), Connection(209, 131), Connection(131, 198), + Connection(169, 135), Connection(135, 150), Connection(150, 169), + Connection(114, 47), Connection(47, 217), Connection(217, 114), + Connection(224, 223), Connection(223, 53), Connection(53, 224), + Connection(220, 45), Connection(45, 134), Connection(134, 220), + Connection(32, 211), Connection(211, 140), Connection(140, 32), + Connection(109, 67), Connection(67, 108), Connection(108, 109), + Connection(146, 43), Connection(43, 91), Connection(91, 146), + Connection(231, 230), Connection(230, 120), Connection(120, 231), + Connection(113, 226), Connection(226, 247), Connection(247, 113), + Connection(105, 63), Connection(63, 52), Connection(52, 105), + Connection(241, 238), Connection(238, 242), Connection(242, 241), + Connection(124, 46), Connection(46, 156), Connection(156, 124), + Connection(95, 78), Connection(78, 96), Connection(96, 95), + Connection(70, 46), Connection(46, 63), Connection(63, 70), + Connection(116, 143), Connection(143, 227), Connection(227, 116), + Connection(116, 123), Connection(123, 111), Connection(111, 116), + Connection(1, 44), Connection(44, 19), Connection(19, 1), + Connection(3, 236), Connection(236, 51), Connection(51, 3), + Connection(207, 216), Connection(216, 205), Connection(205, 207), + Connection(26, 154), Connection(154, 22), Connection(22, 26), + Connection(165, 39), Connection(39, 167), Connection(167, 165), + Connection(199, 200), Connection(200, 208), Connection(208, 199), + Connection(101, 36), Connection(36, 100), Connection(100, 101), + Connection(43, 57), Connection(57, 202), Connection(202, 43), + Connection(242, 20), Connection(20, 99), Connection(99, 242), + Connection(56, 28), Connection(28, 157), Connection(157, 56), + Connection(124, 35), Connection(35, 113), Connection(113, 124), + Connection(29, 160), Connection(160, 27), Connection(27, 29), + Connection(211, 204), Connection(204, 210), Connection(210, 211), + Connection(124, 113), Connection(113, 46), Connection(46, 124), + Connection(106, 43), Connection(43, 204), Connection(204, 106), + Connection(96, 62), Connection(62, 77), Connection(77, 96), + Connection(227, 137), Connection(137, 116), Connection(116, 227), + Connection(73, 41), Connection(41, 72), Connection(72, 73), + Connection(36, 203), Connection(203, 142), Connection(142, 36), + Connection(235, 64), Connection(64, 240), Connection(240, 235), + Connection(48, 49), Connection(49, 64), Connection(64, 48), + Connection(42, 41), Connection(41, 74), Connection(74, 42), + Connection(214, 212), Connection(212, 207), Connection(207, 214), + Connection(183, 42), Connection(42, 184), Connection(184, 183), + Connection(210, 169), Connection(169, 211), Connection(211, 210), + Connection(140, 170), Connection(170, 176), Connection(176, 140), + Connection(104, 105), Connection(105, 69), Connection(69, 104), + Connection(193, 122), Connection(122, 168), Connection(168, 193), + Connection(50, 123), Connection(123, 187), Connection(187, 50), + Connection(89, 96), Connection(96, 90), Connection(90, 89), + Connection(66, 65), Connection(65, 107), Connection(107, 66), + Connection(179, 89), Connection(89, 180), Connection(180, 179), + Connection(119, 101), Connection(101, 120), Connection(120, 119), + Connection(68, 63), Connection(63, 104), Connection(104, 68), + Connection(234, 93), Connection(93, 227), Connection(227, 234), + Connection(16, 15), Connection(15, 85), Connection(85, 16), + Connection(209, 129), Connection(129, 49), Connection(49, 209), + Connection(15, 14), Connection(14, 86), Connection(86, 15), + Connection(107, 55), Connection(55, 9), Connection(9, 107), + Connection(120, 100), Connection(100, 121), Connection(121, 120), + Connection(153, 145), Connection(145, 22), Connection(22, 153), + Connection(178, 88), Connection(88, 179), Connection(179, 178), + Connection(197, 6), Connection(6, 196), Connection(196, 197), + Connection(89, 88), Connection(88, 96), Connection(96, 89), + Connection(135, 138), Connection(138, 136), Connection(136, 135), + Connection(138, 215), Connection(215, 172), Connection(172, 138), + Connection(218, 115), Connection(115, 219), Connection(219, 218), + Connection(41, 42), Connection(42, 81), Connection(81, 41), + Connection(5, 195), Connection(195, 51), Connection(51, 5), + Connection(57, 43), Connection(43, 61), Connection(61, 57), + Connection(208, 171), Connection(171, 199), Connection(199, 208), + Connection(41, 81), Connection(81, 38), Connection(38, 41), + Connection(224, 53), Connection(53, 225), Connection(225, 224), + Connection(24, 144), Connection(144, 110), Connection(110, 24), + Connection(105, 52), Connection(52, 66), Connection(66, 105), + Connection(118, 229), Connection(229, 117), Connection(117, 118), + Connection(227, 34), Connection(34, 234), Connection(234, 227), + Connection(66, 107), Connection(107, 69), Connection(69, 66), + Connection(10, 109), Connection(109, 151), Connection(151, 10), + Connection(219, 48), Connection(48, 235), Connection(235, 219), + Connection(183, 62), Connection(62, 191), Connection(191, 183), + Connection(142, 129), Connection(129, 126), Connection(126, 142), + Connection(116, 111), Connection(111, 143), Connection(143, 116), + Connection(118, 117), Connection(117, 50), Connection(50, 118), + Connection(223, 222), Connection(222, 52), Connection(52, 223), + Connection(94, 19), Connection(19, 141), Connection(141, 94), + Connection(222, 221), Connection(221, 65), Connection(65, 222), + Connection(196, 3), Connection(3, 197), Connection(197, 196), + Connection(45, 220), Connection(220, 44), Connection(44, 45), + Connection(156, 70), Connection(70, 139), Connection(139, 156), + Connection(188, 122), Connection(122, 245), Connection(245, 188), + Connection(139, 71), Connection(71, 162), Connection(162, 139), + Connection(149, 170), Connection(170, 150), Connection(150, 149), + Connection(122, 188), Connection(188, 196), Connection(196, 122), + Connection(206, 216), Connection(216, 92), Connection(92, 206), + Connection(164, 2), Connection(2, 167), Connection(167, 164), + Connection(242, 141), Connection(141, 241), Connection(241, 242), + Connection(0, 164), Connection(164, 37), Connection(37, 0), + Connection(11, 72), Connection(72, 12), Connection(12, 11), + Connection(12, 38), Connection(38, 13), Connection(13, 12), + Connection(70, 63), Connection(63, 71), Connection(71, 70), + Connection(31, 226), Connection(226, 111), Connection(111, 31), + Connection(36, 101), Connection(101, 205), Connection(205, 36), + Connection(203, 206), Connection(206, 165), Connection(165, 203), + Connection(126, 209), Connection(209, 217), Connection(217, 126), + Connection(98, 165), Connection(165, 97), Connection(97, 98), + Connection(237, 220), Connection(220, 218), Connection(218, 237), + Connection(237, 239), Connection(239, 241), Connection(241, 237), + Connection(210, 214), Connection(214, 169), Connection(169, 210), + Connection(140, 171), Connection(171, 32), Connection(32, 140), + Connection(241, 125), Connection(125, 237), Connection(237, 241), + Connection(179, 86), Connection(86, 178), Connection(178, 179), + Connection(180, 85), Connection(85, 179), Connection(179, 180), + Connection(181, 84), Connection(84, 180), Connection(180, 181), + Connection(182, 83), Connection(83, 181), Connection(181, 182), + Connection(194, 201), Connection(201, 182), Connection(182, 194), + Connection(177, 137), Connection(137, 132), Connection(132, 177), + Connection(184, 76), Connection(76, 183), Connection(183, 184), + Connection(185, 61), Connection(61, 184), Connection(184, 185), + Connection(186, 57), Connection(57, 185), Connection(185, 186), + Connection(216, 212), Connection(212, 186), Connection(186, 216), + Connection(192, 214), Connection(214, 187), Connection(187, 192), + Connection(139, 34), Connection(34, 156), Connection(156, 139), + Connection(218, 79), Connection(79, 237), Connection(237, 218), + Connection(147, 123), Connection(123, 177), Connection(177, 147), + Connection(45, 44), Connection(44, 4), Connection(4, 45), + Connection(208, 201), Connection(201, 32), Connection(32, 208), + Connection(98, 64), Connection(64, 129), Connection(129, 98), + Connection(192, 213), Connection(213, 138), Connection(138, 192), + Connection(235, 59), Connection(59, 219), Connection(219, 235), + Connection(141, 242), Connection(242, 97), Connection(97, 141), + Connection(97, 2), Connection(2, 141), Connection(141, 97), + Connection(240, 75), Connection(75, 235), Connection(235, 240), + Connection(229, 24), Connection(24, 228), Connection(228, 229), + Connection(31, 25), Connection(25, 226), Connection(226, 31), + Connection(230, 23), Connection(23, 229), Connection(229, 230), + Connection(231, 22), Connection(22, 230), Connection(230, 231), + Connection(232, 26), Connection(26, 231), Connection(231, 232), + Connection(233, 112), Connection(112, 232), Connection(232, 233), + Connection(244, 189), Connection(189, 243), Connection(243, 244), + Connection(189, 221), Connection(221, 190), Connection(190, 189), + Connection(222, 28), Connection(28, 221), Connection(221, 222), + Connection(223, 27), Connection(27, 222), Connection(222, 223), + Connection(224, 29), Connection(29, 223), Connection(223, 224), + Connection(225, 30), Connection(30, 224), Connection(224, 225), + Connection(113, 247), Connection(247, 225), Connection(225, 113), + Connection(99, 60), Connection(60, 240), Connection(240, 99), + Connection(213, 147), Connection(147, 215), Connection(215, 213), + Connection(60, 20), Connection(20, 166), Connection(166, 60), + Connection(192, 187), Connection(187, 213), Connection(213, 192), + Connection(243, 112), Connection(112, 244), Connection(244, 243), + Connection(244, 233), Connection(233, 245), Connection(245, 244), + Connection(245, 128), Connection(128, 188), Connection(188, 245), + Connection(188, 114), Connection(114, 174), Connection(174, 188), + Connection(134, 131), Connection(131, 220), Connection(220, 134), + Connection(174, 217), Connection(217, 236), Connection(236, 174), + Connection(236, 198), Connection(198, 134), Connection(134, 236), + Connection(215, 177), Connection(177, 58), Connection(58, 215), + Connection(156, 143), Connection(143, 124), Connection(124, 156), + Connection(25, 110), Connection(110, 7), Connection(7, 25), + Connection(31, 228), Connection(228, 25), Connection(25, 31), + Connection(264, 356), Connection(356, 368), Connection(368, 264), + Connection(0, 11), Connection(11, 267), Connection(267, 0), + Connection(451, 452), Connection(452, 349), Connection(349, 451), + Connection(267, 302), Connection(302, 269), Connection(269, 267), + Connection(350, 357), Connection(357, 277), Connection(277, 350), + Connection(350, 452), Connection(452, 357), Connection(357, 350), + Connection(299, 333), Connection(333, 297), Connection(297, 299), + Connection(396, 175), Connection(175, 377), Connection(377, 396), + Connection(280, 347), Connection(347, 330), Connection(330, 280), + Connection(269, 303), Connection(303, 270), Connection(270, 269), + Connection(151, 9), Connection(9, 337), Connection(337, 151), + Connection(344, 278), Connection(278, 360), Connection(360, 344), + Connection(424, 418), Connection(418, 431), Connection(431, 424), + Connection(270, 304), Connection(304, 409), Connection(409, 270), + Connection(272, 310), Connection(310, 407), Connection(407, 272), + Connection(322, 270), Connection(270, 410), Connection(410, 322), + Connection(449, 450), Connection(450, 347), Connection(347, 449), + Connection(432, 422), Connection(422, 434), Connection(434, 432), + Connection(18, 313), Connection(313, 17), Connection(17, 18), + Connection(291, 306), Connection(306, 375), Connection(375, 291), + Connection(259, 387), Connection(387, 260), Connection(260, 259), + Connection(424, 335), Connection(335, 418), Connection(418, 424), + Connection(434, 364), Connection(364, 416), Connection(416, 434), + Connection(391, 423), Connection(423, 327), Connection(327, 391), + Connection(301, 251), Connection(251, 298), Connection(298, 301), + Connection(275, 281), Connection(281, 4), Connection(4, 275), + Connection(254, 373), Connection(373, 253), Connection(253, 254), + Connection(375, 307), Connection(307, 321), Connection(321, 375), + Connection(280, 425), Connection(425, 411), Connection(411, 280), + Connection(200, 421), Connection(421, 18), Connection(18, 200), + Connection(335, 321), Connection(321, 406), Connection(406, 335), + Connection(321, 320), Connection(320, 405), Connection(405, 321), + Connection(314, 315), Connection(315, 17), Connection(17, 314), + Connection(423, 426), Connection(426, 266), Connection(266, 423), + Connection(396, 377), Connection(377, 369), Connection(369, 396), + Connection(270, 322), Connection(322, 269), Connection(269, 270), + Connection(413, 417), Connection(417, 464), Connection(464, 413), + Connection(385, 386), Connection(386, 258), Connection(258, 385), + Connection(248, 456), Connection(456, 419), Connection(419, 248), + Connection(298, 284), Connection(284, 333), Connection(333, 298), + Connection(168, 417), Connection(417, 8), Connection(8, 168), + Connection(448, 346), Connection(346, 261), Connection(261, 448), + Connection(417, 413), Connection(413, 285), Connection(285, 417), + Connection(326, 327), Connection(327, 328), Connection(328, 326), + Connection(277, 355), Connection(355, 329), Connection(329, 277), + Connection(309, 392), Connection(392, 438), Connection(438, 309), + Connection(381, 382), Connection(382, 256), Connection(256, 381), + Connection(279, 429), Connection(429, 360), Connection(360, 279), + Connection(365, 364), Connection(364, 379), Connection(379, 365), + Connection(355, 277), Connection(277, 437), Connection(437, 355), + Connection(282, 443), Connection(443, 283), Connection(283, 282), + Connection(281, 275), Connection(275, 363), Connection(363, 281), + Connection(395, 431), Connection(431, 369), Connection(369, 395), + Connection(299, 297), Connection(297, 337), Connection(337, 299), + Connection(335, 273), Connection(273, 321), Connection(321, 335), + Connection(348, 450), Connection(450, 349), Connection(349, 348), + Connection(359, 446), Connection(446, 467), Connection(467, 359), + Connection(283, 293), Connection(293, 282), Connection(282, 283), + Connection(250, 458), Connection(458, 462), Connection(462, 250), + Connection(300, 276), Connection(276, 383), Connection(383, 300), + Connection(292, 308), Connection(308, 325), Connection(325, 292), + Connection(283, 276), Connection(276, 293), Connection(293, 283), + Connection(264, 372), Connection(372, 447), Connection(447, 264), + Connection(346, 352), Connection(352, 340), Connection(340, 346), + Connection(354, 274), Connection(274, 19), Connection(19, 354), + Connection(363, 456), Connection(456, 281), Connection(281, 363), + Connection(426, 436), Connection(436, 425), Connection(425, 426), + Connection(380, 381), Connection(381, 252), Connection(252, 380), + Connection(267, 269), Connection(269, 393), Connection(393, 267), + Connection(421, 200), Connection(200, 428), Connection(428, 421), + Connection(371, 266), Connection(266, 329), Connection(329, 371), + Connection(432, 287), Connection(287, 422), Connection(422, 432), + Connection(290, 250), Connection(250, 328), Connection(328, 290), + Connection(385, 258), Connection(258, 384), Connection(384, 385), + Connection(446, 265), Connection(265, 342), Connection(342, 446), + Connection(386, 387), Connection(387, 257), Connection(257, 386), + Connection(422, 424), Connection(424, 430), Connection(430, 422), + Connection(445, 342), Connection(342, 276), Connection(276, 445), + Connection(422, 273), Connection(273, 424), Connection(424, 422), + Connection(306, 292), Connection(292, 307), Connection(307, 306), + Connection(352, 366), Connection(366, 345), Connection(345, 352), + Connection(268, 271), Connection(271, 302), Connection(302, 268), + Connection(358, 423), Connection(423, 371), Connection(371, 358), + Connection(327, 294), Connection(294, 460), Connection(460, 327), + Connection(331, 279), Connection(279, 294), Connection(294, 331), + Connection(303, 271), Connection(271, 304), Connection(304, 303), + Connection(436, 432), Connection(432, 427), Connection(427, 436), + Connection(304, 272), Connection(272, 408), Connection(408, 304), + Connection(395, 394), Connection(394, 431), Connection(431, 395), + Connection(378, 395), Connection(395, 400), Connection(400, 378), + Connection(296, 334), Connection(334, 299), Connection(299, 296), + Connection(6, 351), Connection(351, 168), Connection(168, 6), + Connection(376, 352), Connection(352, 411), Connection(411, 376), + Connection(307, 325), Connection(325, 320), Connection(320, 307), + Connection(285, 295), Connection(295, 336), Connection(336, 285), + Connection(320, 319), Connection(319, 404), Connection(404, 320), + Connection(329, 330), Connection(330, 349), Connection(349, 329), + Connection(334, 293), Connection(293, 333), Connection(333, 334), + Connection(366, 323), Connection(323, 447), Connection(447, 366), + Connection(316, 15), Connection(15, 315), Connection(315, 316), + Connection(331, 358), Connection(358, 279), Connection(279, 331), + Connection(317, 14), Connection(14, 316), Connection(316, 317), + Connection(8, 285), Connection(285, 9), Connection(9, 8), + Connection(277, 329), Connection(329, 350), Connection(350, 277), + Connection(253, 374), Connection(374, 252), Connection(252, 253), + Connection(319, 318), Connection(318, 403), Connection(403, 319), + Connection(351, 6), Connection(6, 419), Connection(419, 351), + Connection(324, 318), Connection(318, 325), Connection(325, 324), + Connection(397, 367), Connection(367, 365), Connection(365, 397), + Connection(288, 435), Connection(435, 397), Connection(397, 288), + Connection(278, 344), Connection(344, 439), Connection(439, 278), + Connection(310, 272), Connection(272, 311), Connection(311, 310), + Connection(248, 195), Connection(195, 281), Connection(281, 248), + Connection(375, 273), Connection(273, 291), Connection(291, 375), + Connection(175, 396), Connection(396, 199), Connection(199, 175), + Connection(312, 311), Connection(311, 268), Connection(268, 312), + Connection(276, 283), Connection(283, 445), Connection(445, 276), + Connection(390, 373), Connection(373, 339), Connection(339, 390), + Connection(295, 282), Connection(282, 296), Connection(296, 295), + Connection(448, 449), Connection(449, 346), Connection(346, 448), + Connection(356, 264), Connection(264, 454), Connection(454, 356), + Connection(337, 336), Connection(336, 299), Connection(299, 337), + Connection(337, 338), Connection(338, 151), Connection(151, 337), + Connection(294, 278), Connection(278, 455), Connection(455, 294), + Connection(308, 292), Connection(292, 415), Connection(415, 308), + Connection(429, 358), Connection(358, 355), Connection(355, 429), + Connection(265, 340), Connection(340, 372), Connection(372, 265), + Connection(352, 346), Connection(346, 280), Connection(280, 352), + Connection(295, 442), Connection(442, 282), Connection(282, 295), + Connection(354, 19), Connection(19, 370), Connection(370, 354), + Connection(285, 441), Connection(441, 295), Connection(295, 285), + Connection(195, 248), Connection(248, 197), Connection(197, 195), + Connection(457, 440), Connection(440, 274), Connection(274, 457), + Connection(301, 300), Connection(300, 368), Connection(368, 301), + Connection(417, 351), Connection(351, 465), Connection(465, 417), + Connection(251, 301), Connection(301, 389), Connection(389, 251), + Connection(394, 395), Connection(395, 379), Connection(379, 394), + Connection(399, 412), Connection(412, 419), Connection(419, 399), + Connection(410, 436), Connection(436, 322), Connection(322, 410), + Connection(326, 2), Connection(2, 393), Connection(393, 326), + Connection(354, 370), Connection(370, 461), Connection(461, 354), + Connection(393, 164), Connection(164, 267), Connection(267, 393), + Connection(268, 302), Connection(302, 12), Connection(12, 268), + Connection(312, 268), Connection(268, 13), Connection(13, 312), + Connection(298, 293), Connection(293, 301), Connection(301, 298), + Connection(265, 446), Connection(446, 340), Connection(340, 265), + Connection(280, 330), Connection(330, 425), Connection(425, 280), + Connection(322, 426), Connection(426, 391), Connection(391, 322), + Connection(420, 429), Connection(429, 437), Connection(437, 420), + Connection(393, 391), Connection(391, 326), Connection(326, 393), + Connection(344, 440), Connection(440, 438), Connection(438, 344), + Connection(458, 459), Connection(459, 461), Connection(461, 458), + Connection(364, 434), Connection(434, 394), Connection(394, 364), + Connection(428, 396), Connection(396, 262), Connection(262, 428), + Connection(274, 354), Connection(354, 457), Connection(457, 274), + Connection(317, 316), Connection(316, 402), Connection(402, 317), + Connection(316, 315), Connection(315, 403), Connection(403, 316), + Connection(315, 314), Connection(314, 404), Connection(404, 315), + Connection(314, 313), Connection(313, 405), Connection(405, 314), + Connection(313, 421), Connection(421, 406), Connection(406, 313), + Connection(323, 366), Connection(366, 361), Connection(361, 323), + Connection(292, 306), Connection(306, 407), Connection(407, 292), + Connection(306, 291), Connection(291, 408), Connection(408, 306), + Connection(291, 287), Connection(287, 409), Connection(409, 291), + Connection(287, 432), Connection(432, 410), Connection(410, 287), + Connection(427, 434), Connection(434, 411), Connection(411, 427), + Connection(372, 264), Connection(264, 383), Connection(383, 372), + Connection(459, 309), Connection(309, 457), Connection(457, 459), + Connection(366, 352), Connection(352, 401), Connection(401, 366), + Connection(1, 274), Connection(274, 4), Connection(4, 1), + Connection(418, 421), Connection(421, 262), Connection(262, 418), + Connection(331, 294), Connection(294, 358), Connection(358, 331), + Connection(435, 433), Connection(433, 367), Connection(367, 435), + Connection(392, 289), Connection(289, 439), Connection(439, 392), + Connection(328, 462), Connection(462, 326), Connection(326, 328), + Connection(94, 2), Connection(2, 370), Connection(370, 94), + Connection(289, 305), Connection(305, 455), Connection(455, 289), + Connection(339, 254), Connection(254, 448), Connection(448, 339), + Connection(359, 255), Connection(255, 446), Connection(446, 359), + Connection(254, 253), Connection(253, 449), Connection(449, 254), + Connection(253, 252), Connection(252, 450), Connection(450, 253), + Connection(252, 256), Connection(256, 451), Connection(451, 252), + Connection(256, 341), Connection(341, 452), Connection(452, 256), + Connection(414, 413), Connection(413, 463), Connection(463, 414), + Connection(286, 441), Connection(441, 414), Connection(414, 286), + Connection(286, 258), Connection(258, 441), Connection(441, 286), + Connection(258, 257), Connection(257, 442), Connection(442, 258), + Connection(257, 259), Connection(259, 443), Connection(443, 257), + Connection(259, 260), Connection(260, 444), Connection(444, 259), + Connection(260, 467), Connection(467, 445), Connection(445, 260), + Connection(309, 459), Connection(459, 250), Connection(250, 309), + Connection(305, 289), Connection(289, 290), Connection(290, 305), + Connection(305, 290), Connection(290, 460), Connection(460, 305), + Connection(401, 376), Connection(376, 435), Connection(435, 401), + Connection(309, 250), Connection(250, 392), Connection(392, 309), + Connection(376, 411), Connection(411, 433), Connection(433, 376), + Connection(453, 341), Connection(341, 464), Connection(464, 453), + Connection(357, 453), Connection(453, 465), Connection(465, 357), + Connection(343, 357), Connection(357, 412), Connection(412, 343), + Connection(437, 343), Connection(343, 399), Connection(399, 437), + Connection(344, 360), Connection(360, 440), Connection(440, 344), + Connection(420, 437), Connection(437, 456), Connection(456, 420), + Connection(360, 420), Connection(420, 363), Connection(363, 360), + Connection(361, 401), Connection(401, 288), Connection(288, 361), + Connection(265, 372), Connection(372, 353), Connection(353, 265), + Connection(390, 339), Connection(339, 249), Connection(249, 390), + Connection(339, 448), Connection(448, 255), Connection(255, 339) + ] + + @dataclasses.dataclass class FaceLandmarkerResult: """The face landmarks detection result from FaceLandmarker, where each vector element represents a single face detected in the image. From a036bf70cc014ce5ba435ab53e3aa25b38a7ad0e Mon Sep 17 00:00:00 2001 From: kinaryml Date: Thu, 13 Apr 2023 21:09:01 -0700 Subject: [PATCH 05/30] Removed Activation from ImageSegmenterOptions --- .../tasks/python/test/vision/image_segmenter_test.py | 4 +--- mediapipe/tasks/python/vision/image_segmenter.py | 10 +--------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/mediapipe/tasks/python/test/vision/image_segmenter_test.py b/mediapipe/tasks/python/test/vision/image_segmenter_test.py index aa557281..de0a263f 100644 --- a/mediapipe/tasks/python/test/vision/image_segmenter_test.py +++ b/mediapipe/tasks/python/test/vision/image_segmenter_test.py @@ -15,7 +15,6 @@ import enum import os -from typing import List from unittest import mock from absl.testing import absltest @@ -34,7 +33,6 @@ ImageSegmenterResult = image_segmenter.ImageSegmenterResult _BaseOptions = base_options_module.BaseOptions _Image = image_module.Image _ImageFormat = image_frame.ImageFormat -_Activation = image_segmenter.ImageSegmenterOptions.Activation _ImageSegmenter = image_segmenter.ImageSegmenter _ImageSegmenterOptions = image_segmenter.ImageSegmenterOptions _RUNNING_MODE = vision_task_running_mode.VisionTaskRunningMode @@ -191,7 +189,7 @@ class ImageSegmenterTest(parameterized.TestCase): # Run segmentation on the model in CONFIDENCE_MASK mode. options = _ImageSegmenterOptions( base_options=base_options, output_category_mask=False, - output_confidence_masks=True, activation=_Activation.SOFTMAX + output_confidence_masks=True ) with _ImageSegmenter.create_from_options(options) as segmenter: diff --git a/mediapipe/tasks/python/vision/image_segmenter.py b/mediapipe/tasks/python/vision/image_segmenter.py index 10277317..c5204db4 100644 --- a/mediapipe/tasks/python/vision/image_segmenter.py +++ b/mediapipe/tasks/python/vision/image_segmenter.py @@ -78,16 +78,10 @@ class ImageSegmenterOptions: is set to the live stream mode. """ - class Activation(enum.Enum): - NONE = 0 - SIGMOID = 1 - SOFTMAX = 2 - base_options: _BaseOptions running_mode: _RunningMode = _RunningMode.IMAGE output_confidence_masks: bool = True output_category_mask: bool = False - activation: Optional[Activation] = Activation.NONE result_callback: Optional[ Callable[[ImageSegmenterResult, image_module.Image, int], None] ] = None @@ -99,9 +93,7 @@ class ImageSegmenterOptions: base_options_proto.use_stream_mode = ( False if self.running_mode == _RunningMode.IMAGE else True ) - segmenter_options_proto = _SegmenterOptionsProto( - activation=self.activation.value - ) + segmenter_options_proto = _SegmenterOptionsProto() return _ImageSegmenterGraphOptionsProto( base_options=base_options_proto, segmenter_options=segmenter_options_proto, From a745b71f97aa49a5533ddb282bfee2bd30654aa4 Mon Sep 17 00:00:00 2001 From: kinaryml Date: Thu, 13 Apr 2023 21:11:20 -0700 Subject: [PATCH 06/30] Removed unused import --- mediapipe/tasks/python/vision/image_segmenter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mediapipe/tasks/python/vision/image_segmenter.py b/mediapipe/tasks/python/vision/image_segmenter.py index c5204db4..8edabe32 100644 --- a/mediapipe/tasks/python/vision/image_segmenter.py +++ b/mediapipe/tasks/python/vision/image_segmenter.py @@ -14,7 +14,6 @@ """MediaPipe image segmenter task.""" import dataclasses -import enum from typing import Callable, List, Mapping, Optional from mediapipe.python import packet_creator From 7b1b94416afca062bbea92dc566198a66e1f19f4 Mon Sep 17 00:00:00 2001 From: Prianka Liz Kariat Date: Fri, 14 Apr 2023 12:13:46 +0530 Subject: [PATCH 07/30] Updated timestamp variable name --- .../ios/vision/core/sources/MPPVisionPacketCreator.h | 8 ++++---- .../ios/vision/core/sources/MPPVisionPacketCreator.mm | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mediapipe/tasks/ios/vision/core/sources/MPPVisionPacketCreator.h b/mediapipe/tasks/ios/vision/core/sources/MPPVisionPacketCreator.h index eaf059ad..ed07c6d9 100644 --- a/mediapipe/tasks/ios/vision/core/sources/MPPVisionPacketCreator.h +++ b/mediapipe/tasks/ios/vision/core/sources/MPPVisionPacketCreator.h @@ -41,7 +41,7 @@ * timestamp. * * @param image The image to send to the MediaPipe graph. - * @param timestampMs The timestamp (in milliseconds) to assign to the packet. + * @param timestampInMilliseconds The timestamp (in milliseconds) to assign to the packet. * @param error Pointer to the memory location where errors if any should be saved. If @c NULL, no * error will be saved. * @@ -49,7 +49,7 @@ * occurred during the conversion. */ + (mediapipe::Packet)createPacketWithMPPImage:(MPPImage *)image - timestampMs:(NSInteger)timestampMs + timestampInMilliseconds:(NSInteger)timestampInMilliseconds error:(NSError **)error; /** @@ -66,11 +66,11 @@ * specified timestamp. * * @param image The `NormalizedRect` to send to the MediaPipe graph. - * @param timestampMs The timestamp (in milliseconds) to assign to the packet. + * @param timestampInMilliseconds The timestamp (in milliseconds) to assign to the packet. * * @return The MediaPipe packet containing the normalized rect. */ + (mediapipe::Packet)createPacketWithNormalizedRect:(mediapipe::NormalizedRect &)normalizedRect - timestampMs:(NSInteger)timestampMs; + timestampInMilliseconds:(NSInteger)timestampInMilliseconds; @end diff --git a/mediapipe/tasks/ios/vision/core/sources/MPPVisionPacketCreator.mm b/mediapipe/tasks/ios/vision/core/sources/MPPVisionPacketCreator.mm index bf136a75..af419c6d 100644 --- a/mediapipe/tasks/ios/vision/core/sources/MPPVisionPacketCreator.mm +++ b/mediapipe/tasks/ios/vision/core/sources/MPPVisionPacketCreator.mm @@ -42,7 +42,7 @@ using ::mediapipe::Timestamp; } + (Packet)createPacketWithMPPImage:(MPPImage *)image - timestampMs:(NSInteger)timestampMs + timestampInMilliseconds:(NSInteger)timestampInMilliseconds error:(NSError **)error { std::unique_ptr imageFrame = [image imageFrameWithError:error]; @@ -51,7 +51,7 @@ using ::mediapipe::Timestamp; } return MakePacket(std::move(imageFrame)) - .At(Timestamp(int64(timestampMs * kMicroSecondsPerMilliSecond))); + .At(Timestamp(int64(timestampInMilliseconds * kMicroSecondsPerMilliSecond))); } + (Packet)createPacketWithNormalizedRect:(NormalizedRect &)normalizedRect { @@ -59,9 +59,9 @@ using ::mediapipe::Timestamp; } + (Packet)createPacketWithNormalizedRect:(NormalizedRect &)normalizedRect - timestampMs:(NSInteger)timestampMs { + timestampInMilliseconds:(NSInteger)timestampInMilliseconds { return MakePacket(std::move(normalizedRect)) - .At(Timestamp(int64(timestampMs * kMicroSecondsPerMilliSecond))); + .At(Timestamp(int64(timestampInMilliseconds * kMicroSecondsPerMilliSecond))); } @end From f5197a3adc3d5ee309cbca02b6a32c5699eee5ac Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Fri, 14 Apr 2023 13:00:44 -0700 Subject: [PATCH 08/30] Expose get_graph_config function for task_runner. PiperOrigin-RevId: 524365682 --- mediapipe/tasks/python/core/pybind/task_runner.cc | 5 +++++ mediapipe/tasks/python/vision/core/base_vision_task_api.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/mediapipe/tasks/python/core/pybind/task_runner.cc b/mediapipe/tasks/python/core/pybind/task_runner.cc index aa48a1a9..250c0fa6 100644 --- a/mediapipe/tasks/python/core/pybind/task_runner.cc +++ b/mediapipe/tasks/python/core/pybind/task_runner.cc @@ -204,6 +204,11 @@ This can be useful for resetting a stateful task graph to process new data. Raises: RuntimeError: The underlying medipaipe graph fails to reset and restart. )doc"); + + task_runner.def( + "get_graph_config", + [](TaskRunner* self) { return self->GetGraphConfig(); }, + R"doc(Returns the canonicalized CalculatorGraphConfig of the underlying graph.)doc"); } } // namespace python diff --git a/mediapipe/tasks/python/vision/core/base_vision_task_api.py b/mediapipe/tasks/python/vision/core/base_vision_task_api.py index eb976153..d9195d3c 100644 --- a/mediapipe/tasks/python/vision/core/base_vision_task_api.py +++ b/mediapipe/tasks/python/vision/core/base_vision_task_api.py @@ -208,6 +208,11 @@ class BaseVisionTaskApi(object): """ self._runner.close() + def get_graph_config(self) -> calculator_pb2.CalculatorGraphConfig: + """Returns the canonicalized CalculatorGraphConfig of the underlying graph. + """ + return self._runner.get_graph_config() + def __enter__(self): """Return `self` upon entering the runtime context.""" return self From 92f45c98d866642e6c466474bfd444477e51f218 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Fri, 14 Apr 2023 13:23:03 -0700 Subject: [PATCH 09/30] Support new output format for ImageSegmenter PiperOrigin-RevId: 524371021 --- .../tasks/web/vision/core/render_utils.ts | 7 +- .../tasks/web/vision/image_segmenter/BUILD | 5 +- .../vision/image_segmenter/image_segmenter.ts | 213 +++++++++++------- .../image_segmenter_options.d.ts | 19 +- .../image_segmenter_result.d.ts | 37 +++ .../image_segmenter/image_segmenter_test.ts | 140 ++++++++---- 6 files changed, 268 insertions(+), 153 deletions(-) create mode 100644 mediapipe/tasks/web/vision/image_segmenter/image_segmenter_result.d.ts diff --git a/mediapipe/tasks/web/vision/core/render_utils.ts b/mediapipe/tasks/web/vision/core/render_utils.ts index 879e2301..903d789f 100644 --- a/mediapipe/tasks/web/vision/core/render_utils.ts +++ b/mediapipe/tasks/web/vision/core/render_utils.ts @@ -59,13 +59,12 @@ export function drawCategoryMask( const isFloatArray = image instanceof Float32Array; for (let i = 0; i < image.length; i++) { const colorIndex = isFloatArray ? Math.round(image[i] * 255) : image[i]; - const color = COLOR_MAP[colorIndex]; + let color = COLOR_MAP[colorIndex % COLOR_MAP.length]; - // When we're given a confidence mask by accident, we just log and return. - // TODO: We should fix this. if (!color) { + // TODO: We should fix this. console.warn('No color for ', colorIndex); - return; + color = COLOR_MAP[colorIndex % COLOR_MAP.length]; } rgbaArray[4 * i] = color[0]; diff --git a/mediapipe/tasks/web/vision/image_segmenter/BUILD b/mediapipe/tasks/web/vision/image_segmenter/BUILD index a4b9008d..3db15641 100644 --- a/mediapipe/tasks/web/vision/image_segmenter/BUILD +++ b/mediapipe/tasks/web/vision/image_segmenter/BUILD @@ -29,7 +29,10 @@ mediapipe_ts_library( mediapipe_ts_declaration( name = "image_segmenter_types", - srcs = ["image_segmenter_options.d.ts"], + srcs = [ + "image_segmenter_options.d.ts", + "image_segmenter_result.d.ts", + ], deps = [ "//mediapipe/tasks/web/core", "//mediapipe/tasks/web/core:classifier_options", diff --git a/mediapipe/tasks/web/vision/image_segmenter/image_segmenter.ts b/mediapipe/tasks/web/vision/image_segmenter/image_segmenter.ts index 3690fd85..74004776 100644 --- a/mediapipe/tasks/web/vision/image_segmenter/image_segmenter.ts +++ b/mediapipe/tasks/web/vision/image_segmenter/image_segmenter.ts @@ -22,33 +22,48 @@ import {ImageSegmenterGraphOptions as ImageSegmenterGraphOptionsProto} from '../ import {SegmenterOptions as SegmenterOptionsProto} from '../../../../tasks/cc/vision/image_segmenter/proto/segmenter_options_pb'; import {WasmFileset} from '../../../../tasks/web/core/wasm_fileset'; import {ImageProcessingOptions} from '../../../../tasks/web/vision/core/image_processing_options'; -import {SegmentationMask, SegmentationMaskCallback} from '../../../../tasks/web/vision/core/types'; +import {SegmentationMask} from '../../../../tasks/web/vision/core/types'; import {VisionGraphRunner, VisionTaskRunner} from '../../../../tasks/web/vision/core/vision_task_runner'; import {LabelMapItem} from '../../../../util/label_map_pb'; import {ImageSource, WasmModule} from '../../../../web/graph_runner/graph_runner'; // Placeholder for internal dependency on trusted resource url import {ImageSegmenterOptions} from './image_segmenter_options'; +import {ImageSegmenterResult} from './image_segmenter_result'; export * from './image_segmenter_options'; -export {SegmentationMask, SegmentationMaskCallback}; +export * from './image_segmenter_result'; +export {SegmentationMask}; export {ImageSource}; // Used in the public API const IMAGE_STREAM = 'image_in'; const NORM_RECT_STREAM = 'norm_rect'; -const GROUPED_SEGMENTATIONS_STREAM = 'segmented_masks'; +const CONFIDENCE_MASKS_STREAM = 'confidence_masks'; +const CATEGORY_MASK_STREAM = 'category_mask'; const IMAGE_SEGMENTER_GRAPH = 'mediapipe.tasks.vision.image_segmenter.ImageSegmenterGraph'; const TENSORS_TO_SEGMENTATION_CALCULATOR_NAME = 'mediapipe.tasks.TensorsToSegmentationCalculator'; +const DEFAULT_OUTPUT_CATEGORY_MASK = false; +const DEFAULT_OUTPUT_CONFIDENCE_MASKS = true; // The OSS JS API does not support the builder pattern. // tslint:disable:jspb-use-builder-pattern +/** + * A callback that receives the computed masks from the image segmenter. The + * returned data is only valid for the duration of the callback. If + * asynchronous processing is needed, all data needs to be copied before the + * callback returns. + */ +export type ImageSegmenterCallack = (result: ImageSegmenterResult) => void; + /** Performs image segmentation on images. */ export class ImageSegmenter extends VisionTaskRunner { - private userCallback: SegmentationMaskCallback = () => {}; + private result: ImageSegmenterResult = {width: 0, height: 0}; private labels: string[] = []; + private outputCategoryMask = DEFAULT_OUTPUT_CATEGORY_MASK; + private outputConfidenceMasks = DEFAULT_OUTPUT_CONFIDENCE_MASKS; private readonly options: ImageSegmenterGraphOptionsProto; private readonly segmenterOptions: SegmenterOptionsProto; @@ -109,7 +124,6 @@ export class ImageSegmenter extends VisionTaskRunner { this.options.setBaseOptions(new BaseOptionsProto()); } - protected override get baseOptions(): BaseOptionsProto { return this.options.getBaseOptions()!; } @@ -137,12 +151,14 @@ export class ImageSegmenter extends VisionTaskRunner { this.options.clearDisplayNamesLocale(); } - if (options.outputType === 'CONFIDENCE_MASK') { - this.segmenterOptions.setOutputType( - SegmenterOptionsProto.OutputType.CONFIDENCE_MASK); - } else { - this.segmenterOptions.setOutputType( - SegmenterOptionsProto.OutputType.CATEGORY_MASK); + if ('outputCategoryMask' in options) { + this.outputCategoryMask = + options.outputCategoryMask ?? DEFAULT_OUTPUT_CATEGORY_MASK; + } + + if ('outputConfidenceMasks' in options) { + this.outputConfidenceMasks = + options.outputConfidenceMasks ?? DEFAULT_OUTPUT_CONFIDENCE_MASKS; } return super.applyOptions(options); @@ -192,7 +208,7 @@ export class ImageSegmenter extends VisionTaskRunner { * lifetime of the returned data is only guaranteed for the duration of the * callback. */ - segment(image: ImageSource, callback: SegmentationMaskCallback): void; + segment(image: ImageSource, callback: ImageSegmenterCallack): void; /** * Performs image segmentation on the provided single image and invokes the * callback with the response. The method returns synchronously once the @@ -208,22 +224,77 @@ export class ImageSegmenter extends VisionTaskRunner { */ segment( image: ImageSource, imageProcessingOptions: ImageProcessingOptions, - callback: SegmentationMaskCallback): void; + callback: ImageSegmenterCallack): void; segment( image: ImageSource, imageProcessingOptionsOrCallback: ImageProcessingOptions| - SegmentationMaskCallback, - callback?: SegmentationMaskCallback): void { + ImageSegmenterCallack, + callback?: ImageSegmenterCallack): void { const imageProcessingOptions = typeof imageProcessingOptionsOrCallback !== 'function' ? imageProcessingOptionsOrCallback : {}; - - this.userCallback = typeof imageProcessingOptionsOrCallback === 'function' ? + const userCallback = + typeof imageProcessingOptionsOrCallback === 'function' ? imageProcessingOptionsOrCallback : callback!; + + this.reset(); this.processImageData(image, imageProcessingOptions); - this.userCallback = () => {}; + userCallback(this.result); + } + + /** + * Performs image segmentation on the provided video frame and invokes the + * callback with the response. The method returns synchronously once the + * callback returns. Only use this method when the ImageSegmenter is + * created with running mode `video`. + * + * @param videoFrame A video frame to process. + * @param timestamp The timestamp of the current frame, in ms. + * @param callback The callback that is invoked with the segmented masks. The + * lifetime of the returned data is only guaranteed for the duration of the + * callback. + */ + segmentForVideo( + videoFrame: ImageSource, timestamp: number, + callback: ImageSegmenterCallack): void; + /** + * Performs image segmentation on the provided video frame and invokes the + * callback with the response. The method returns synchronously once the + * callback returns. Only use this method when the ImageSegmenter is + * created with running mode `video`. + * + * @param videoFrame A video frame to process. + * @param imageProcessingOptions the `ImageProcessingOptions` specifying how + * to process the input image before running inference. + * @param timestamp The timestamp of the current frame, in ms. + * @param callback The callback that is invoked with the segmented masks. The + * lifetime of the returned data is only guaranteed for the duration of the + * callback. + */ + segmentForVideo( + videoFrame: ImageSource, imageProcessingOptions: ImageProcessingOptions, + timestamp: number, callback: ImageSegmenterCallack): void; + segmentForVideo( + videoFrame: ImageSource, + timestampOrImageProcessingOptions: number|ImageProcessingOptions, + timestampOrCallback: number|ImageSegmenterCallack, + callback?: ImageSegmenterCallack): void { + const imageProcessingOptions = + typeof timestampOrImageProcessingOptions !== 'number' ? + timestampOrImageProcessingOptions : + {}; + const timestamp = typeof timestampOrImageProcessingOptions === 'number' ? + timestampOrImageProcessingOptions : + timestampOrCallback as number; + const userCallback = typeof timestampOrCallback === 'function' ? + timestampOrCallback : + callback!; + + this.reset(); + this.processVideoData(videoFrame, imageProcessingOptions, timestamp); + userCallback(this.result); } /** @@ -241,56 +312,8 @@ export class ImageSegmenter extends VisionTaskRunner { return this.labels; } - /** - * Performs image segmentation on the provided video frame and invokes the - * callback with the response. The method returns synchronously once the - * callback returns. Only use this method when the ImageSegmenter is - * created with running mode `video`. - * - * @param videoFrame A video frame to process. - * @param timestamp The timestamp of the current frame, in ms. - * @param callback The callback that is invoked with the segmented masks. The - * lifetime of the returned data is only guaranteed for the duration of the - * callback. - */ - segmentForVideo( - videoFrame: ImageSource, timestamp: number, - callback: SegmentationMaskCallback): void; - /** - * Performs image segmentation on the provided video frame and invokes the - * callback with the response. The method returns synchronously once the - * callback returns. Only use this method when the ImageSegmenter is - * created with running mode `video`. - * - * @param videoFrame A video frame to process. - * @param imageProcessingOptions the `ImageProcessingOptions` specifying how - * to process the input image before running inference. - * @param timestamp The timestamp of the current frame, in ms. - * @param callback The callback that is invoked with the segmented masks. The - * lifetime of the returned data is only guaranteed for the duration of the - * callback. - */ - segmentForVideo( - videoFrame: ImageSource, imageProcessingOptions: ImageProcessingOptions, - timestamp: number, callback: SegmentationMaskCallback): void; - segmentForVideo( - videoFrame: ImageSource, - timestampOrImageProcessingOptions: number|ImageProcessingOptions, - timestampOrCallback: number|SegmentationMaskCallback, - callback?: SegmentationMaskCallback): void { - const imageProcessingOptions = - typeof timestampOrImageProcessingOptions !== 'number' ? - timestampOrImageProcessingOptions : - {}; - const timestamp = typeof timestampOrImageProcessingOptions === 'number' ? - timestampOrImageProcessingOptions : - timestampOrCallback as number; - - this.userCallback = typeof timestampOrCallback === 'function' ? - timestampOrCallback : - callback!; - this.processVideoData(videoFrame, imageProcessingOptions, timestamp); - this.userCallback = () => {}; + private reset(): void { + this.result = {width: 0, height: 0}; } /** Updates the MediaPipe graph configuration. */ @@ -298,7 +321,6 @@ export class ImageSegmenter extends VisionTaskRunner { const graphConfig = new CalculatorGraphConfig(); graphConfig.addInputStream(IMAGE_STREAM); graphConfig.addInputStream(NORM_RECT_STREAM); - graphConfig.addOutputStream(GROUPED_SEGMENTATIONS_STREAM); const calculatorOptions = new CalculatorOptions(); calculatorOptions.setExtension( @@ -308,26 +330,47 @@ export class ImageSegmenter extends VisionTaskRunner { segmenterNode.setCalculator(IMAGE_SEGMENTER_GRAPH); segmenterNode.addInputStream('IMAGE:' + IMAGE_STREAM); segmenterNode.addInputStream('NORM_RECT:' + NORM_RECT_STREAM); - segmenterNode.addOutputStream( - 'GROUPED_SEGMENTATION:' + GROUPED_SEGMENTATIONS_STREAM); segmenterNode.setOptions(calculatorOptions); graphConfig.addNode(segmenterNode); - this.graphRunner.attachImageVectorListener( - GROUPED_SEGMENTATIONS_STREAM, (masks, timestamp) => { - if (masks.length === 0) { - this.userCallback([], 0, 0); - } else { - this.userCallback( - masks.map(m => m.data), masks[0].width, masks[0].height); - } - this.setLatestOutputTimestamp(timestamp); - }); - this.graphRunner.attachEmptyPacketListener( - GROUPED_SEGMENTATIONS_STREAM, timestamp => { - this.setLatestOutputTimestamp(timestamp); - }); + if (this.outputConfidenceMasks) { + graphConfig.addOutputStream(CONFIDENCE_MASKS_STREAM); + segmenterNode.addOutputStream( + 'CONFIDENCE_MASKS:' + CONFIDENCE_MASKS_STREAM); + + this.graphRunner.attachImageVectorListener( + CONFIDENCE_MASKS_STREAM, (masks, timestamp) => { + this.result.confidenceMasks = masks.map(m => m.data); + if (masks.length >= 0) { + this.result.width = masks[0].width; + this.result.height = masks[0].height; + } + + this.setLatestOutputTimestamp(timestamp); + }); + this.graphRunner.attachEmptyPacketListener( + CONFIDENCE_MASKS_STREAM, timestamp => { + this.setLatestOutputTimestamp(timestamp); + }); + } + + if (this.outputCategoryMask) { + graphConfig.addOutputStream(CATEGORY_MASK_STREAM); + segmenterNode.addOutputStream('CATEGORY_MASK:' + CATEGORY_MASK_STREAM); + + this.graphRunner.attachImageListener( + CATEGORY_MASK_STREAM, (mask, timestamp) => { + this.result.categoryMask = mask.data; + this.result.width = mask.width; + this.result.height = mask.height; + this.setLatestOutputTimestamp(timestamp); + }); + this.graphRunner.attachEmptyPacketListener( + CATEGORY_MASK_STREAM, timestamp => { + this.setLatestOutputTimestamp(timestamp); + }); + } const binaryGraph = graphConfig.serializeBinary(); this.setGraph(new Uint8Array(binaryGraph), /* isBinary= */ true); diff --git a/mediapipe/tasks/web/vision/image_segmenter/image_segmenter_options.d.ts b/mediapipe/tasks/web/vision/image_segmenter/image_segmenter_options.d.ts index c17e7e42..f80a792a 100644 --- a/mediapipe/tasks/web/vision/image_segmenter/image_segmenter_options.d.ts +++ b/mediapipe/tasks/web/vision/image_segmenter/image_segmenter_options.d.ts @@ -24,18 +24,9 @@ export interface ImageSegmenterOptions extends VisionTaskOptions { */ displayNamesLocale?: string|undefined; - /** - * The output type of segmentation results. - * - * The two supported modes are: - * - Category Mask: Gives a single output mask where each pixel represents - * the class which the pixel in the original image was - * predicted to belong to. - * - Confidence Mask: Gives a list of output masks (one for each class). For - * each mask, the pixel represents the prediction - * confidence, usually in the [0.0, 0.1] range. - * - * Defaults to `CATEGORY_MASK`. - */ - outputType?: 'CATEGORY_MASK'|'CONFIDENCE_MASK'|undefined; + /** Whether to output confidence masks. Defaults to true. */ + outputConfidenceMasks?: boolean|undefined; + + /** Whether to output the category masks. Defaults to false. */ + outputCategoryMask?: boolean|undefined; } diff --git a/mediapipe/tasks/web/vision/image_segmenter/image_segmenter_result.d.ts b/mediapipe/tasks/web/vision/image_segmenter/image_segmenter_result.d.ts new file mode 100644 index 00000000..be082d51 --- /dev/null +++ b/mediapipe/tasks/web/vision/image_segmenter/image_segmenter_result.d.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2023 The MediaPipe Authors. All Rights Reserved. + * + * 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. + */ + +/** The output result of ImageSegmenter. */ +export declare interface ImageSegmenterResult { + /** + * Multiple masks as Float32Arrays or WebGLTextures where, for each mask, each + * pixel represents the prediction confidence, usually in the [0, 1] range. + */ + confidenceMasks?: Float32Array[]|WebGLTexture[]; + + /** + * A category mask as a Uint8ClampedArray or WebGLTexture where each + * pixel represents the class which the pixel in the original image was + * predicted to belong to. + */ + categoryMask?: Uint8ClampedArray|WebGLTexture; + + /** The width of the masks. */ + width: number; + + /** The height of the masks. */ + height: number; +} diff --git a/mediapipe/tasks/web/vision/image_segmenter/image_segmenter_test.ts b/mediapipe/tasks/web/vision/image_segmenter/image_segmenter_test.ts index 4cf27b9a..6b5c9008 100644 --- a/mediapipe/tasks/web/vision/image_segmenter/image_segmenter_test.ts +++ b/mediapipe/tasks/web/vision/image_segmenter/image_segmenter_test.ts @@ -18,7 +18,7 @@ import 'jasmine'; // Placeholder for internal dependency on encodeByteArray import {CalculatorGraphConfig} from '../../../../framework/calculator_pb'; -import {addJasmineCustomFloatEqualityTester, createSpyWasmModule, MediapipeTasksFake, SpyWasmModule, verifyGraph, verifyListenersRegistered} from '../../../../tasks/web/core/task_runner_test_utils'; +import {addJasmineCustomFloatEqualityTester, createSpyWasmModule, MediapipeTasksFake, SpyWasmModule, verifyGraph} from '../../../../tasks/web/core/task_runner_test_utils'; import {WasmImage} from '../../../../web/graph_runner/graph_runner_image_lib'; import {ImageSegmenter} from './image_segmenter'; @@ -30,7 +30,9 @@ class ImageSegmenterFake extends ImageSegmenter implements MediapipeTasksFake { graph: CalculatorGraphConfig|undefined; fakeWasmModule: SpyWasmModule; - imageVectorListener: + categoryMaskListener: + ((images: WasmImage, timestamp: number) => void)|undefined; + confidenceMasksListener: ((images: WasmImage[], timestamp: number) => void)|undefined; constructor() { @@ -38,11 +40,16 @@ class ImageSegmenterFake extends ImageSegmenter implements MediapipeTasksFake { this.fakeWasmModule = this.graphRunner.wasmModule as unknown as SpyWasmModule; - this.attachListenerSpies[0] = + this.attachListenerSpies[0] = spyOn(this.graphRunner, 'attachImageListener') + .and.callFake((stream, listener) => { + expect(stream).toEqual('category_mask'); + this.categoryMaskListener = listener; + }); + this.attachListenerSpies[1] = spyOn(this.graphRunner, 'attachImageVectorListener') .and.callFake((stream, listener) => { - expect(stream).toEqual('segmented_masks'); - this.imageVectorListener = listener; + expect(stream).toEqual('confidence_masks'); + this.confidenceMasksListener = listener; }); spyOn(this.graphRunner, 'setGraph').and.callFake(binaryGraph => { this.graph = CalculatorGraphConfig.deserializeBinary(binaryGraph); @@ -63,17 +70,18 @@ describe('ImageSegmenter', () => { it('initializes graph', async () => { verifyGraph(imageSegmenter); - verifyListenersRegistered(imageSegmenter); + + // Verify default options + expect(imageSegmenter.categoryMaskListener).not.toBeDefined(); + expect(imageSegmenter.confidenceMasksListener).toBeDefined(); }); it('reloads graph when settings are changed', async () => { await imageSegmenter.setOptions({displayNamesLocale: 'en'}); verifyGraph(imageSegmenter, ['displayNamesLocale', 'en']); - verifyListenersRegistered(imageSegmenter); await imageSegmenter.setOptions({displayNamesLocale: 'de'}); verifyGraph(imageSegmenter, ['displayNamesLocale', 'de']); - verifyListenersRegistered(imageSegmenter); }); it('can use custom models', async () => { @@ -100,9 +108,11 @@ describe('ImageSegmenter', () => { }); it('merges options', async () => { - await imageSegmenter.setOptions({outputType: 'CATEGORY_MASK'}); + await imageSegmenter.setOptions( + {baseOptions: {modelAssetBuffer: new Uint8Array([])}}); await imageSegmenter.setOptions({displayNamesLocale: 'en'}); - verifyGraph(imageSegmenter, [['segmenterOptions', 'outputType'], 1]); + verifyGraph( + imageSegmenter, [['baseOptions', 'modelAsset', 'fileContent'], '']); verifyGraph(imageSegmenter, ['displayNamesLocale', 'en']); }); @@ -115,22 +125,13 @@ describe('ImageSegmenter', () => { defaultValue: unknown; } - const testCases: TestCase[] = [ - { - optionName: 'displayNamesLocale', - fieldPath: ['displayNamesLocale'], - userValue: 'en', - graphValue: 'en', - defaultValue: 'en' - }, - { - optionName: 'outputType', - fieldPath: ['segmenterOptions', 'outputType'], - userValue: 'CONFIDENCE_MASK', - graphValue: 2, - defaultValue: 1 - }, - ]; + const testCases: TestCase[] = [{ + optionName: 'displayNamesLocale', + fieldPath: ['displayNamesLocale'], + userValue: 'en', + graphValue: 'en', + defaultValue: 'en' + }]; for (const testCase of testCases) { it(`can set ${testCase.optionName}`, async () => { @@ -158,27 +159,31 @@ describe('ImageSegmenter', () => { }).toThrowError('This task doesn\'t support region-of-interest.'); }); - it('supports category masks', (done) => { + it('supports category mask', async () => { const mask = new Uint8ClampedArray([1, 2, 3, 4]); + await imageSegmenter.setOptions( + {outputCategoryMask: true, outputConfidenceMasks: false}); + // Pass the test data to our listener imageSegmenter.fakeWasmModule._waitUntilIdle.and.callFake(() => { - verifyListenersRegistered(imageSegmenter); - imageSegmenter.imageVectorListener!( - [ - {data: mask, width: 2, height: 2}, - ], - /* timestamp= */ 1337); + expect(imageSegmenter.categoryMaskListener).toBeDefined(); + imageSegmenter.categoryMaskListener! + ({data: mask, width: 2, height: 2}, + /* timestamp= */ 1337); }); // Invoke the image segmenter - imageSegmenter.segment({} as HTMLImageElement, (masks, width, height) => { - expect(imageSegmenter.fakeWasmModule._waitUntilIdle).toHaveBeenCalled(); - expect(masks).toHaveSize(1); - expect(masks[0]).toEqual(mask); - expect(width).toEqual(2); - expect(height).toEqual(2); - done(); + + return new Promise(resolve => { + imageSegmenter.segment({} as HTMLImageElement, result => { + expect(imageSegmenter.fakeWasmModule._waitUntilIdle).toHaveBeenCalled(); + expect(result.categoryMask).toEqual(mask); + expect(result.confidenceMasks).not.toBeDefined(); + expect(result.width).toEqual(2); + expect(result.height).toEqual(2); + resolve(); + }); }); }); @@ -186,12 +191,13 @@ describe('ImageSegmenter', () => { const mask1 = new Float32Array([0.1, 0.2, 0.3, 0.4]); const mask2 = new Float32Array([0.5, 0.6, 0.7, 0.8]); - await imageSegmenter.setOptions({outputType: 'CONFIDENCE_MASK'}); + await imageSegmenter.setOptions( + {outputCategoryMask: false, outputConfidenceMasks: true}); // Pass the test data to our listener imageSegmenter.fakeWasmModule._waitUntilIdle.and.callFake(() => { - verifyListenersRegistered(imageSegmenter); - imageSegmenter.imageVectorListener!( + expect(imageSegmenter.confidenceMasksListener).toBeDefined(); + imageSegmenter.confidenceMasksListener!( [ {data: mask1, width: 2, height: 2}, {data: mask2, width: 2, height: 2}, @@ -201,13 +207,49 @@ describe('ImageSegmenter', () => { return new Promise(resolve => { // Invoke the image segmenter - imageSegmenter.segment({} as HTMLImageElement, (masks, width, height) => { + imageSegmenter.segment({} as HTMLImageElement, result => { expect(imageSegmenter.fakeWasmModule._waitUntilIdle).toHaveBeenCalled(); - expect(masks).toHaveSize(2); - expect(masks[0]).toEqual(mask1); - expect(masks[1]).toEqual(mask2); - expect(width).toEqual(2); - expect(height).toEqual(2); + expect(result.categoryMask).not.toBeDefined(); + expect(result.confidenceMasks).toEqual([mask1, mask2]); + expect(result.width).toEqual(2); + expect(result.height).toEqual(2); + resolve(); + }); + }); + }); + + it('supports combined category and confidence masks', async () => { + const categoryMask = new Uint8ClampedArray([1, 0]); + const confidenceMask1 = new Float32Array([0.0, 1.0]); + const confidenceMask2 = new Float32Array([1.0, 0.0]); + + await imageSegmenter.setOptions( + {outputCategoryMask: true, outputConfidenceMasks: true}); + + // Pass the test data to our listener + imageSegmenter.fakeWasmModule._waitUntilIdle.and.callFake(() => { + expect(imageSegmenter.categoryMaskListener).toBeDefined(); + expect(imageSegmenter.confidenceMasksListener).toBeDefined(); + imageSegmenter.categoryMaskListener! + ({data: categoryMask, width: 1, height: 1}, 1337); + imageSegmenter.confidenceMasksListener!( + [ + {data: confidenceMask1, width: 1, height: 1}, + {data: confidenceMask2, width: 1, height: 1}, + ], + 1337); + }); + + return new Promise(resolve => { + // Invoke the image segmenter + imageSegmenter.segment({} as HTMLImageElement, result => { + expect(imageSegmenter.fakeWasmModule._waitUntilIdle).toHaveBeenCalled(); + expect(result.categoryMask).toEqual(categoryMask); + expect(result.confidenceMasks).toEqual([ + confidenceMask1, confidenceMask2 + ]); + expect(result.width).toEqual(1); + expect(result.height).toEqual(1); resolve(); }); }); From abd6574c6d4504ab95e55527d8e7a048c093b31b Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Fri, 14 Apr 2023 13:47:36 -0700 Subject: [PATCH 10/30] Internal MediaPipe Tasks change. PiperOrigin-RevId: 524377097 --- .../cc/text/custom_ops/sentencepiece/BUILD | 70 ++++++ .../sentencepiece/model_converter.cc | 131 ++++++++++ .../sentencepiece/model_converter.h | 33 +++ .../sentencepiece/optimized_encoder.cc | 236 ++++++++++++++++++ .../sentencepiece/optimized_encoder.h | 46 ++++ .../sentencepiece/optimized_encoder_test.cc | 171 +++++++++++++ .../sentencepiece/sentencepiece_constants.h | 38 +++ .../testdata/sentencepiece.model | Bin 0 -> 330106 bytes 8 files changed, 725 insertions(+) create mode 100644 mediapipe/tasks/cc/text/custom_ops/sentencepiece/model_converter.cc create mode 100644 mediapipe/tasks/cc/text/custom_ops/sentencepiece/model_converter.h create mode 100644 mediapipe/tasks/cc/text/custom_ops/sentencepiece/optimized_encoder.cc create mode 100644 mediapipe/tasks/cc/text/custom_ops/sentencepiece/optimized_encoder.h create mode 100644 mediapipe/tasks/cc/text/custom_ops/sentencepiece/optimized_encoder_test.cc create mode 100644 mediapipe/tasks/cc/text/custom_ops/sentencepiece/sentencepiece_constants.h create mode 100644 mediapipe/tasks/cc/text/custom_ops/sentencepiece/testdata/sentencepiece.model diff --git a/mediapipe/tasks/cc/text/custom_ops/sentencepiece/BUILD b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/BUILD index 072b21f5..a1833ac5 100644 --- a/mediapipe/tasks/cc/text/custom_ops/sentencepiece/BUILD +++ b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/BUILD @@ -18,6 +18,13 @@ package(default_visibility = ["//mediapipe/tasks:internal"]) licenses(["notice"]) +filegroup( + name = "testdata", + srcs = glob([ + "testdata/**", + ]), +) + filegroup( name = "config_fbs", srcs = ["config.fbs"], @@ -80,3 +87,66 @@ cc_test( "//mediapipe/framework/port:gtest_main", ], ) + +cc_library( + name = "sentencepiece_constants", + hdrs = ["sentencepiece_constants.h"], +) + +cc_library( + name = "model_converter", + srcs = [ + "model_converter.cc", + ], + hdrs = [ + "model_converter.h", + ], + deps = [ + ":config", + ":double_array_trie_builder", + ":encoder_config", + ":sentencepiece_constants", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_sentencepiece//src:sentencepiece_model_cc_proto", + ], +) + +cc_library( + name = "optimized_encoder", + srcs = [ + "optimized_encoder.cc", + ], + hdrs = [ + "optimized_encoder.h", + ], + deps = [ + ":double_array_trie", + ":encoder_config", + ":utils", + ], +) + +cc_test( + name = "optimized_encoder_test", + srcs = [ + "optimized_encoder_test.cc", + ], + data = [ + ":testdata", + ], + deps = [ + ":double_array_trie_builder", + ":encoder_config", + ":model_converter", + ":optimized_encoder", + "//mediapipe/framework/deps:file_path", + "//mediapipe/framework/port:gtest_main", + "@com_google_absl//absl/flags:flag", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings:str_format", + "@com_google_sentencepiece//src:sentencepiece_cc_proto", + "@com_google_sentencepiece//src:sentencepiece_processor", + "@org_tensorflow//tensorflow/core:lib", + ], +) diff --git a/mediapipe/tasks/cc/text/custom_ops/sentencepiece/model_converter.cc b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/model_converter.cc new file mode 100644 index 00000000..3a831f3d --- /dev/null +++ b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/model_converter.cc @@ -0,0 +1,131 @@ +/* Copyright 2023 The MediaPipe Authors. All Rights Reserved. + +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/tasks/cc/text/custom_ops/sentencepiece/model_converter.h" + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "mediapipe/tasks/cc/text/custom_ops/sentencepiece/double_array_trie_builder.h" +#include "mediapipe/tasks/cc/text/custom_ops/sentencepiece/encoder_config_generated.h" +#include "mediapipe/tasks/cc/text/custom_ops/sentencepiece/sentencepiece_constants.h" +#include "src/sentencepiece_model.pb.h" + +namespace mediapipe::tflite_operations::sentencepiece { + +std::tuple, std::vector> +DecodePrecompiledCharsmap( + const ::sentencepiece::NormalizerSpec& normalizer_spec) { + // This function "undoes" encoding done by + // sentencepiece::normalizer::Normalizer::EncodePrecompiledCharsMap. + const char* precompiled_map = normalizer_spec.precompiled_charsmap().data(); + const uint32_t trie_size = + *reinterpret_cast(precompiled_map); + const uint32_t* trie_ptr = + reinterpret_cast(precompiled_map + sizeof(uint32_t)); + const int8_t* normalized_ptr = reinterpret_cast( + precompiled_map + sizeof(uint32_t) + trie_size); + const int normalized_size = normalizer_spec.precompiled_charsmap().length() - + sizeof(uint32_t) - trie_size; + return std::make_tuple( + std::vector(trie_ptr, trie_ptr + trie_size / sizeof(uint32_t)), + std::vector(normalized_ptr, normalized_ptr + normalized_size)); +} + +absl::StatusOr ConvertSentencepieceModelToFlatBuffer( + const std::string& model_config_str, int encoding_offset) { + ::sentencepiece::ModelProto model_config; + if (!model_config.ParseFromString(model_config_str)) { + return absl::InvalidArgumentError( + "Invalid configuration, can't parse SentencePiece model config " + + model_config.InitializationErrorString()); + } + // Convert sentencepieces. + std::vector pieces; + pieces.reserve(model_config.pieces_size()); + std::vector scores; + scores.reserve(model_config.pieces_size()); + std::vector ids; + ids.reserve(model_config.pieces_size()); + float min_score = 0.0; + int index = 0; + for (const auto& piece : model_config.pieces()) { + switch (piece.type()) { + case ::sentencepiece::ModelProto::SentencePiece::NORMAL: + case ::sentencepiece::ModelProto::SentencePiece::USER_DEFINED: + pieces.push_back(piece.piece()); + ids.push_back(index); + if (piece.score() < min_score) { + min_score = piece.score(); + } + break; + case ::sentencepiece::ModelProto::SentencePiece::UNKNOWN: + case ::sentencepiece::ModelProto::SentencePiece::CONTROL: + // Ignore unknown and control codes. + break; + default: + return absl::InvalidArgumentError("Invalid SentencePiece piece type " + + piece.piece()); + } + scores.push_back(piece.score()); + ++index; + } + flatbuffers::FlatBufferBuilder builder(1024); + const auto pieces_trie_vector = builder.CreateVector(BuildTrie(pieces, ids)); + const auto pieces_score_vector = builder.CreateVector(scores); + TrieBuilder pieces_trie_builder(builder); + pieces_trie_builder.add_nodes(pieces_trie_vector); + const auto pieces_trie_fbs = pieces_trie_builder.Finish(); + + // Converting normalization. + const auto normalization = + DecodePrecompiledCharsmap(model_config.normalizer_spec()); + const auto normalization_trie = std::get<0>(normalization); + const auto normalization_strings = std::get<1>(normalization); + const auto normalization_trie_vector = + builder.CreateVector(normalization_trie); + TrieBuilder normalization_trie_builder(builder); + normalization_trie_builder.add_nodes(normalization_trie_vector); + const auto normalization_trie_fbs = normalization_trie_builder.Finish(); + const auto normalization_strings_fbs = + builder.CreateVector(normalization_strings); + + EncoderConfigBuilder ecb(builder); + ecb.add_version(EncoderVersion::EncoderVersion_SENTENCE_PIECE); + ecb.add_start_code(model_config.trainer_spec().bos_id()); + ecb.add_end_code(model_config.trainer_spec().eos_id()); + ecb.add_unknown_code(model_config.trainer_spec().unk_id()); + ecb.add_unknown_penalty(min_score - kUnkPenalty); + ecb.add_encoding_offset(encoding_offset); + ecb.add_pieces(pieces_trie_fbs); + ecb.add_pieces_scores(pieces_score_vector); + ecb.add_remove_extra_whitespaces( + model_config.normalizer_spec().remove_extra_whitespaces()); + ecb.add_add_dummy_prefix(model_config.normalizer_spec().add_dummy_prefix()); + ecb.add_escape_whitespaces( + model_config.normalizer_spec().escape_whitespaces()); + ecb.add_normalized_prefixes(normalization_trie_fbs); + ecb.add_normalized_replacements(normalization_strings_fbs); + FinishEncoderConfigBuffer(builder, ecb.Finish()); + return std::string(reinterpret_cast(builder.GetBufferPointer()), + builder.GetSize()); +} + +std::string ConvertSentencepieceModel(const std::string& model_string) { + const auto result = ConvertSentencepieceModelToFlatBuffer(model_string); + assert(result.status().ok()); + return result.value(); +} + +} // namespace mediapipe::tflite_operations::sentencepiece diff --git a/mediapipe/tasks/cc/text/custom_ops/sentencepiece/model_converter.h b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/model_converter.h new file mode 100644 index 00000000..828db16d --- /dev/null +++ b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/model_converter.h @@ -0,0 +1,33 @@ +/* Copyright 2023 The MediaPipe Authors. All Rights Reserved. + +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_TASKS_CC_TEXT_CUSTOM_OPS_SENTENCEPIECE_MODEL_CONVERTER_H_ +#define MEDIAPIPE_TASKS_CC_TEXT_CUSTOM_OPS_SENTENCEPIECE_MODEL_CONVERTER_H_ + +#include + +#include "absl/status/statusor.h" + +namespace mediapipe::tflite_operations::sentencepiece { + +// Converts Sentencepiece configuration to flatbuffer format. +// encoding_offset is used by some encoders that combine different encodings. +absl::StatusOr ConvertSentencepieceModelToFlatBuffer( + const std::string& model_config_str, int encoding_offset = 0); +std::string ConvertSentencepieceModel(const std::string& model_string); + +} // namespace mediapipe::tflite_operations::sentencepiece + +#endif // MEDIAPIPE_TASKS_CC_TEXT_CUSTOM_OPS_SENTENCEPIECE_MODEL_CONVERTER_H_ diff --git a/mediapipe/tasks/cc/text/custom_ops/sentencepiece/optimized_encoder.cc b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/optimized_encoder.cc new file mode 100644 index 00000000..365b1a5a --- /dev/null +++ b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/optimized_encoder.cc @@ -0,0 +1,236 @@ +/* Copyright 2023 The MediaPipe Authors. All Rights Reserved. + +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/tasks/cc/text/custom_ops/sentencepiece/optimized_encoder.h" + +#include +#include + +#include "mediapipe/tasks/cc/text/custom_ops/sentencepiece/double_array_trie.h" +#include "mediapipe/tasks/cc/text/custom_ops/sentencepiece/encoder_config_generated.h" +#include "mediapipe/tasks/cc/text/custom_ops/sentencepiece/utils.h" + +namespace mediapipe::tflite_operations::sentencepiece { +namespace { + +const char kSpaceSymbol[] = "\xe2\x96\x81"; + +template +std::tuple> process_string( + const std::string& input, const std::vector& offsets, + const processing_callback& pc) { + std::string result_string; + result_string.reserve(input.size()); + std::vector result_offsets; + result_offsets.reserve(offsets.size()); + for (int i = 0, j = 0; i < input.size();) { + auto result = pc(input.data() + i, input.size() - i); + auto consumed = std::get<0>(result); + auto new_string = std::get<1>(result); + if (consumed == 0) { + // Skip the current byte and move forward. + result_string.push_back(input[i]); + result_offsets.push_back(offsets[j]); + i++; + j++; + continue; + } + result_string.append(new_string.data(), new_string.length()); + for (int i = 0; i < new_string.length(); ++i) { + result_offsets.push_back(offsets[j]); + } + j += consumed; + i += consumed; + } + return std::make_tuple(result_string, result_offsets); +} + +inline char is_whitespace(char c) { + return c == ' ' || c == '\t' || c == '\r' || c == '\n'; +} + +std::tuple remove_extra_whitespaces(const char* data, + int len) { + if (len == 0 || !is_whitespace(*data)) { + return std::make_tuple(0, utils::string_view(nullptr, 0)); + } + int num_consumed = 1; + for (; num_consumed < len && is_whitespace(data[num_consumed]); + ++num_consumed) { + } + return num_consumed > 1 + ? std::make_tuple(num_consumed, utils::string_view(" ", 1)) + : std::make_tuple(0, utils::string_view(nullptr, 0)); +} + +std::tuple find_replacement( + const char* data, int len, const DoubleArrayTrie& dat, + const flatbuffers::Vector& replacements) { + const auto max_match = dat.LongestPrefixMatch(utils::string_view(data, len)); + if (!max_match.empty()) { + // Because flatbuffer byte is signed char which is not the same as char, + // there is the reinterpret_cast here. + const char* replaced_string_ptr = + reinterpret_cast(replacements.data() + max_match.id); + return std::make_tuple(max_match.match_length, + utils::string_view(replaced_string_ptr)); + } + return std::make_tuple(0, utils::string_view(nullptr, 0)); +} +} // namespace + +std::tuple> NormalizeString( + const std::string& in_string, const EncoderConfig& config) { + std::vector output_offsets; + std::string result = in_string; + output_offsets.reserve(in_string.length()); + for (int i = 0; i < in_string.length(); ++i) { + output_offsets.push_back(i); + } + if (in_string.empty()) { + return std::make_tuple(result, output_offsets); + } + if (config.add_dummy_prefix()) { + result.insert(result.begin(), ' '); + output_offsets.insert(output_offsets.begin(), 0); + } + // Greedely replace normalized_prefixes with normalized_replacements + if (config.normalized_prefixes() != nullptr && + config.normalized_replacements() != nullptr) { + const DoubleArrayTrie normalized_prefixes_matcher( + config.normalized_prefixes()->nodes()); + const auto norm_replace = [&config, &normalized_prefixes_matcher]( + const char* data, int len) { + return find_replacement(data, len, normalized_prefixes_matcher, + *config.normalized_replacements()); + }; + std::tie(result, output_offsets) = + process_string(result, output_offsets, norm_replace); + } + if (config.remove_extra_whitespaces()) { + std::tie(result, output_offsets) = + process_string(result, output_offsets, remove_extra_whitespaces); + if (!result.empty() && is_whitespace(result.back())) { + result.pop_back(); + output_offsets.pop_back(); + } + } + if (config.escape_whitespaces()) { + const auto replace_whitespaces = [](const char* data, int len) { + if (len > 0 && is_whitespace(*data)) { + return std::make_tuple(1, utils::string_view(kSpaceSymbol)); + } + return std::make_tuple(0, utils::string_view(nullptr, 0)); + }; + std::tie(result, output_offsets) = + process_string(result, output_offsets, replace_whitespaces); + } + + return std::make_tuple(result, output_offsets); +} + +EncoderResult EncodeNormalizedString(const std::string& str, + const std::vector& offsets, + const EncoderConfig& config, bool add_bos, + bool add_eos, bool reverse) { + const DoubleArrayTrie piece_matcher(config.pieces()->nodes()); + const flatbuffers::Vector* piece_scores = config.pieces_scores(); + const int unknown_code = config.unknown_code(); + const float unknown_penalty = config.unknown_penalty(); + struct LatticeElement { + float score = 0; + int code = -1; + int prev_position = -1; + LatticeElement(float score_, int code_, int prev_position_) + : score(score_), code(code_), prev_position(prev_position_) {} + LatticeElement() {} + }; + const int length = str.length(); + std::vector lattice(length + 1); + for (int i = 0; i < length; ++i) { + if (i > 0 && lattice[i].prev_position < 0) { + // This state is unreachable. + continue; + } + if (unknown_code >= 0) { + // Put unknown code. + const float penalized_score = lattice[i].score + unknown_penalty; + const int pos = i + 1; + LatticeElement& current_element = lattice[pos]; + if (current_element.prev_position < 0 || + current_element.score < penalized_score) { + current_element = LatticeElement( + penalized_score, unknown_code, + // If the current state is already reached by unknown code, merge + // states. + lattice[i].code == unknown_code ? lattice[i].prev_position : i); + } + } + auto lattice_update = [&lattice, i, + piece_scores](const DoubleArrayTrie::Match& m) { + LatticeElement& target_element = lattice[i + m.match_length]; + const float score = lattice[i].score + (*piece_scores)[m.id]; + if (target_element.prev_position < 0 || target_element.score < score) { + target_element = LatticeElement(score, m.id, i); + } + }; + piece_matcher.IteratePrefixMatches( + utils::string_view(str.data() + i, length - i), lattice_update); + } + + EncoderResult result; + if (add_eos) { + result.codes.push_back(config.end_code()); + result.offsets.push_back(length); + } + if (lattice[length].prev_position >= 0) { + for (int pos = length; pos > 0;) { + auto code = lattice[pos].code; + if (code != config.unknown_code()) { + code += config.encoding_offset(); + } + result.codes.push_back(code); + pos = lattice[pos].prev_position; + result.offsets.push_back(offsets[pos]); + } + } + if (add_bos) { + result.codes.push_back(config.start_code()); + result.offsets.push_back(0); + } + if (!reverse) { + std::reverse(result.codes.begin(), result.codes.end()); + std::reverse(result.offsets.begin(), result.offsets.end()); + } + return result; +} + +EncoderResult EncodeString(const std::string& string, const void* config_buffer, + bool add_bos, bool add_eos, bool reverse) { + // Get the config from the buffer. + const EncoderConfig* config = GetEncoderConfig(config_buffer); + if (config->version() != EncoderVersion::EncoderVersion_SENTENCE_PIECE) { + EncoderResult result; + result.type = EncoderResultType::WRONG_CONFIG; + return result; + } + std::string normalized_string; + std::vector offsets; + std::tie(normalized_string, offsets) = NormalizeString(string, *config); + return EncodeNormalizedString(normalized_string, offsets, *config, add_bos, + add_eos, reverse); +} + +} // namespace mediapipe::tflite_operations::sentencepiece diff --git a/mediapipe/tasks/cc/text/custom_ops/sentencepiece/optimized_encoder.h b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/optimized_encoder.h new file mode 100644 index 00000000..849a4784 --- /dev/null +++ b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/optimized_encoder.h @@ -0,0 +1,46 @@ +/* Copyright 2023 The MediaPipe Authors. All Rights Reserved. + +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_TASKS_CC_TEXT_CUSTOM_OPS_SENTENCEPIECE_OPTIMIZED_ENCODER_H_ +#define MEDIAPIPE_TASKS_CC_TEXT_CUSTOM_OPS_SENTENCEPIECE_OPTIMIZED_ENCODER_H_ + +// Sentencepiece encoder optimized with memmapped model. + +#include +#include +#include + +#include "mediapipe/tasks/cc/text/custom_ops/sentencepiece/encoder_config_generated.h" + +namespace mediapipe::tflite_operations::sentencepiece { + +enum class EncoderResultType { SUCCESS = 0, WRONG_CONFIG = 1 }; + +struct EncoderResult { + EncoderResultType type = EncoderResultType::SUCCESS; + std::vector codes; + std::vector offsets; +}; +std::tuple> NormalizeString( + const std::string& in_string, const EncoderConfig& config); + +// Encodes one string and returns ids and offsets. Takes the configuration as a +// type-erased buffer. +EncoderResult EncodeString(const std::string& string, const void* config_buffer, + bool add_bos, bool add_eos, bool reverse); + +} // namespace mediapipe::tflite_operations::sentencepiece + +#endif // MEDIAPIPE_TASKS_CC_TEXT_CUSTOM_OPS_SENTENCEPIECE_OPTIMIZED_ENCODER_H_ diff --git a/mediapipe/tasks/cc/text/custom_ops/sentencepiece/optimized_encoder_test.cc b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/optimized_encoder_test.cc new file mode 100644 index 00000000..e65bd185 --- /dev/null +++ b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/optimized_encoder_test.cc @@ -0,0 +1,171 @@ +/* Copyright 2023 The MediaPipe Authors. All Rights Reserved. + +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/tasks/cc/text/custom_ops/sentencepiece/optimized_encoder.h" + +#include + +#include "absl/flags/flag.h" +#include "absl/status/status.h" +#include "absl/strings/str_format.h" +#include "mediapipe/framework/deps/file_path.h" +#include "mediapipe/framework/port/gmock.h" +#include "mediapipe/framework/port/gtest.h" +#include "mediapipe/tasks/cc/text/custom_ops/sentencepiece/double_array_trie_builder.h" +#include "mediapipe/tasks/cc/text/custom_ops/sentencepiece/encoder_config_generated.h" +#include "mediapipe/tasks/cc/text/custom_ops/sentencepiece/model_converter.h" +#include "src/sentencepiece.pb.h" +#include "src/sentencepiece_processor.h" +#include "tensorflow/core/platform/env.h" + +namespace mediapipe::tflite_operations::sentencepiece { + +namespace internal { + +tensorflow::Status TFReadFileToString(const std::string& filepath, + std::string* data) { + return tensorflow::ReadFileToString(tensorflow::Env::Default(), filepath, + data); +} + +absl::Status StdReadFileToString(const std::string& filepath, + std::string* data) { + std::ifstream infile(filepath); + if (!infile.is_open()) { + return absl::NotFoundError( + absl::StrFormat("Error when opening %s", filepath)); + } + std::string contents((std::istreambuf_iterator(infile)), + (std::istreambuf_iterator())); + data->append(contents); + infile.close(); + return absl::OkStatus(); +} +} // namespace internal + +namespace { + +using ::mediapipe::file::JoinPath; + +static char kConfigFilePath[] = + "/mediapipe/tasks/cc/text/custom_ops/" + "sentencepiece/testdata/sentencepiece.model"; + +TEST(OptimizedEncoder, NormalizeStringWhitestpaces) { + flatbuffers::FlatBufferBuilder builder(1024); + EncoderConfigBuilder ecb(builder); + ecb.add_remove_extra_whitespaces(true); + ecb.add_add_dummy_prefix(true); + ecb.add_escape_whitespaces(true); + FinishEncoderConfigBuffer(builder, ecb.Finish()); + const EncoderConfig* config = GetEncoderConfig(builder.GetBufferPointer()); + { + const auto result = NormalizeString("x y", *config); + const auto res_string = std::get<0>(result); + const auto offsets = std::get<1>(result); + EXPECT_EQ(res_string, "\xe2\x96\x81x\xe2\x96\x81y"); + EXPECT_THAT(offsets, ::testing::ElementsAre(0, 0, 0, 0, 1, 1, 1, 3)); + } + { + const auto result = NormalizeString("\tx y\n", *config); + const auto res_string = std::get<0>(result); + const auto offsets = std::get<1>(result); + EXPECT_EQ(res_string, "\xe2\x96\x81x\xe2\x96\x81y"); + EXPECT_THAT(offsets, ::testing::ElementsAre(0, 0, 0, 1, 2, 2, 2, 4)); + } +} + +TEST(OptimizedEncoder, NormalizeStringReplacement) { + flatbuffers::FlatBufferBuilder builder(1024); + const std::vector norm_prefixes = {"A", "AA", "AAA", "AAAA"}; + const char norm_replacements[] = "A1\0A2\0A3\0A4"; + const auto trie_vector = + builder.CreateVector(BuildTrie(norm_prefixes, {0, 3, 6, 9})); + const auto norm_r = builder.CreateVector( + reinterpret_cast(norm_replacements), + sizeof(norm_replacements)); + TrieBuilder trie_builder(builder); + trie_builder.add_nodes(trie_vector); + const auto norm_p = trie_builder.Finish(); + EncoderConfigBuilder ecb(builder); + ecb.add_remove_extra_whitespaces(false); + ecb.add_normalized_prefixes(norm_p); + ecb.add_normalized_replacements(norm_r); + FinishEncoderConfigBuffer(builder, ecb.Finish()); + const EncoderConfig* config = GetEncoderConfig(builder.GetBufferPointer()); + { + const auto result = NormalizeString("ABAABAAABAAAA", *config); + const auto res_string = std::get<0>(result); + const auto offsets = std::get<1>(result); + EXPECT_EQ(res_string, "A1BA2BA3BA4"); + EXPECT_THAT(offsets, + ::testing::ElementsAre(0, 0, 1, 2, 2, 4, 5, 5, 8, 9, 9)); + } +} + +TEST(OptimizedEncoder, NormalizeStringWhitespacesRemove) { + flatbuffers::FlatBufferBuilder builder(1024); + const std::vector norm_prefixes = {"A", "AA", "AAA", "AAAA", + "X"}; + const char norm_replacements[] = "A1\0A2\0A3\0A4\0 "; + const auto trie_vector = + builder.CreateVector(BuildTrie(norm_prefixes, {0, 3, 6, 9, 12})); + const auto norm_r = builder.CreateVector( + reinterpret_cast(norm_replacements), + sizeof(norm_replacements)); + TrieBuilder trie_builder(builder); + trie_builder.add_nodes(trie_vector); + const auto norm_p = trie_builder.Finish(); + EncoderConfigBuilder ecb(builder); + ecb.add_remove_extra_whitespaces(true); + ecb.add_normalized_prefixes(norm_p); + ecb.add_normalized_replacements(norm_r); + FinishEncoderConfigBuffer(builder, ecb.Finish()); + const EncoderConfig* config = GetEncoderConfig(builder.GetBufferPointer()); + { + const auto result = NormalizeString("XXABAABAAABAAAA", *config); + const auto res_string = std::get<0>(result); + const auto offsets = std::get<1>(result); + EXPECT_EQ(res_string, " A1BA2BA3BA4"); + EXPECT_THAT(offsets, + ::testing::ElementsAre(0, 2, 2, 3, 4, 4, 6, 7, 7, 10, 11, 11)); + } +} + +TEST(OptimizedEncoder, ConfigConverter) { + std::string config; + auto status = + internal::TFReadFileToString(JoinPath("./", kConfigFilePath), &config); + ASSERT_TRUE(status.ok()); + + ::sentencepiece::SentencePieceProcessor processor; + ASSERT_TRUE(processor.LoadFromSerializedProto(config).ok()); + const auto converted_model = ConvertSentencepieceModel(config); + const std::string test_string("Hello world!\\xF0\\x9F\\x8D\\x95"); + const auto encoded = + EncodeString(test_string, converted_model.data(), false, false, false); + ASSERT_EQ(encoded.codes.size(), encoded.offsets.size()); + + ::sentencepiece::SentencePieceText reference_encoded; + ASSERT_TRUE(processor.Encode(test_string, &reference_encoded).ok()); + EXPECT_EQ(encoded.codes.size(), reference_encoded.pieces_size()); + for (int i = 0; i < encoded.codes.size(); ++i) { + EXPECT_EQ(encoded.codes[i], reference_encoded.pieces(i).id()); + EXPECT_EQ(encoded.offsets[i], reference_encoded.pieces(i).begin()); + } +} + +} // namespace +} // namespace mediapipe::tflite_operations::sentencepiece diff --git a/mediapipe/tasks/cc/text/custom_ops/sentencepiece/sentencepiece_constants.h b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/sentencepiece_constants.h new file mode 100644 index 00000000..faf48184 --- /dev/null +++ b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/sentencepiece_constants.h @@ -0,0 +1,38 @@ +/* Copyright 2023 The MediaPipe Authors. All Rights Reserved. + +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_TASKS_CC_TEXT_CUSTOM_OPS_SENTENCEPIECE_SENTENCEPIECE_CONSTANTS_H_ +#define MEDIAPIPE_TASKS_CC_TEXT_CUSTOM_OPS_SENTENCEPIECE_SENTENCEPIECE_CONSTANTS_H_ + +namespace mediapipe::tflite_operations::sentencepiece { + +// The constant is copied from +// https://github.com/google/sentencepiece/blob/master/src/unigram_model.cc +constexpr float kUnkPenalty = 10.0; + +// These constants are copied from +// https://github.com/google/sentencepiece/blob/master/src/sentencepiece_processor.cc +// +// Replaces white space with U+2581 (LOWER ONE EIGHT BLOCK). +constexpr char kSpaceSymbol[] = "\xe2\x96\x81"; + +// Encodes into U+2047 (DOUBLE QUESTION MARK), +// since this character can be useful both for user and +// developer. We can easily figure out that is emitted. +constexpr char kDefaultUnknownSymbol[] = " \xE2\x81\x87 "; + +} // namespace mediapipe::tflite_operations::sentencepiece + +#endif // MEDIAPIPE_TASKS_CC_TEXT_CUSTOM_OPS_SENTENCEPIECE_SENTENCEPIECE_CONSTANTS_H_ diff --git a/mediapipe/tasks/cc/text/custom_ops/sentencepiece/testdata/sentencepiece.model b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/testdata/sentencepiece.model new file mode 100644 index 0000000000000000000000000000000000000000..041188ffd89a500b1353e49054c3ecb3f9dc69b8 GIT binary patch literal 330106 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∋}@hj^y^SmEqmG6^p@cmbm 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 z)BmUcPye6(KmC9D|MdUq|I`1c|4;v){y+VH`v3I*>HpLJr~gm?pZ-7nfBOIQ|LOnJ z|EK>?|DXOp{eSxZ^#AGq)BmUcPye6(KmC9D|MdUq|I`1c|4;v){y+VH`v3I*>HpLJ zr~gm?pZ-7nfBOIQ|LOnJ|EK>?|DXOp{eSxZ^#AGq)BmUcPye6(KmC9D|MdUq|I`1c z|4;v){y+VH`v3I*>HpLJr~gm?pZ-7nfBOIQ|LOnJ|EK>?|DXOp{eSxZ^#AGq)BmUc zPye6(KmC9D|MdUq|I`1c|4;v){y+VH`v3I*>HpLJr~gm?pZ-7nfBOIQ|LOnJ|EK>? z|DXOp{eSxZ^#AGq)BmUcPye6(KmC9D|MdUq|I`1c|4;v){y+VH`v3I*>HpLJr~gm? zpZ-7nfBOIQ|LOnJ|EK>?|DXOp{eSxZ^#AGq)BmUcPye6(KmC9D|MdUq|I`1c|4;v) z{y+VH`v3I*>HpLJr~gm?pZ-7nfBOIQ|LOnJ|EK>?|DXOp{eSxZ^#AGq)BmUcPye6( zKmC9D|MdUq|I`1c|4;v){y+VH`v3I*>HpLJr~gm?pZ-7nfBOIQ|LOnJ|EK>?|DXOp z{eSxZ^#AGq)BmUcPye6(KmC9D|MdUq|I`1c|4;v){y+VH`v3I*>HpLJr~gm?pZ-7n zfBOIQ|LOnJ|EK>?|DXOp{eSxZ^#AGq)BmUcPye6(KmC9D|MdUq|I`1c|4;v){y+VH z`v3I*>HpLJr~gm?pZ-7nfBOIQ|LOnJ|EK>?|DXOp{eSxZ^#AGq)BmUcPye6(KmC9D z|MdUq|I`1c|4;v){y+VH`v3I*>HpLJr~gm?pZ-7nfBOIQ|LOnJ|EK>?|DXOp{eSxZ z^#AGq)BmUcPye6(KmC9D|MdUq|I`1c|4;v){y+VH`v3I*>HpLJr~gm?pZ-7nfBOIQ z|LOnJ|EK>?|DXOp{eSxZ^#AGq)BmUcPye6(KmC9D|MdUq|I`1c|4;v){y+VH`v3I* z>HpLJr~gm?pZ-7nfBOIQ|LOnJ|EK>?|DXOp{eSxZ^#AGq)BmUcPye6(KmC9D|MdUq z|I`1c|4;v){y+VH`v3I*>HpLJr~gm?pZ-7nfBOIQ|LOnJ|EK>?|DXOp{eSxZ^#AGq z)BmUcPye6(KmC9D|MdUq|I`1c|4;v){y+VH`v3I*>HpLJr~gm?pZ-7nfBOIQ|LOnJ z|EK>?|DXOp{eSxZ^#AGq)BmUcPye6(KmC9D|MdUq|I`1c|4;v){y+VH`v3I*>HpLJ zr~gm?pZ-7nfBOIQ|LOnJ|EK>?|DXOp{eSxZ^#AGq)BmUcPye6(KmC9D|MdUq|I`1c z|4;v){y+VH`v3I*>HpLJr~gm?pZ-7nfBOIQ|LOnJ|EK>?|DXOp{eSxZ^#AGq)BmUc zPye6(KmC9D|MdUq|I`1c|4;v){y+VH`v3I*>HpLJr~gm?pZ-7nfBOIQ|LOnJ|EK>? z|DXOp{eSxZ^#AGq)BmUcPye6(KmC9D|MdUq|I`1c|4;v){y+VH`v3I*>HpLJr~gm? zpZ-7nfBOIQ|LOnJ|EK>?|DXOp{eSxZ^#AGq)BmUcPye6(KmC9D|MdUq|I`1c|4;v) z{y+VH`v3I*>HpLJr~gm?pZ-7nfBOIQ|LOnJ|EK>?|DXOp{eSxZ^#AGq)BmUcPye6( zKmC9D|MdUq|I`1c|4;v){y+VH`v3I*>HpLJr~gm?pZ-7nfBOIQ|LOnJ|EK>?|DXOp z{eSxZ^#AGq)BmUcPye6(KmC9D|MdUq|I`1c|4;v){y+VH`v3I*>HpLJr~gm?pZ-7n zfBOIQ|LOnJ|EK>?|DXOp{eSxZ^#AGq)BmUcPye6(KmC9D|MdUq|I`1c|4;v){y+VH z`v3I*>HpLJr~gm?pZ-7nfBOIQ|LOnJ|EK>?|DXOp{eSxZ^#AGq)BmUcPye6(KmC9D z|MdUq|I`1c|4;v){y+VH`v3I*>HpLJr~gm?pZ-7nfBOIQ|LOnJ|EK>?|DXOp{eSxZ z^#AGq)BmUcPye6(KmC9D|MdUq|I`1c|4;v){y+VH`v3I*>HpLJr~gm?pZ-7nfBOIQ z|LOnJ|EK>?|DXOp{eSxZ^#AGq)BmUcPye6(KmC9D|MdUq|I`1c|4;v){y+VH`v3I* z>HpLJr~gm?pZ-7nfBOIQ|LOnJ|EK>?|DXOp{eSxZ^#AGq)BmUcPye6(KmC9D|MdUq z|I`1c|4;v){y+VH`v3I*>HpLJr~gm?pZ-7nfBOIQ|LOnJ|EK>?|DXOp{eSxZ^#AGq z)BmUcPye6(KmC9D|MdUq|I`1c|4;v){y+VH`v3I*>HpLJr~gm?pZ-7nfBOIQ|LOnJ z|EK>?|DXOp{eSxZ^#AGq)BmUcPye6(KmC9D|MdUq|I`1c|4;v){y+VH`v3I*>HpLJ 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 zAR0h4fM@{G0HOgz1BeC?4ImmoG=OLT(Ey?WL<5Kh5Dg$2Ks1180MP)V0Yn3c1`rJ( z8bCCFXaLawq5(t$hz1Z1AR0h4fM@{G0HOgz1BeC?4ImmoG=OLT(Ey?WL<5Kh5Dg$2 zKs1180MP)V0Yn3c1`rJ(8bCCFXaLawq5(t$hz1Z1AR0h4fM@{G0HOgz1BeC?4Immo zG=OLT(Ey?WL<5Kh5Dg$2Ks1180MP)V0Yn3c1`rJ(8bCCFXaLawq5(t$hz1Z1AR0h4 zfM@{G0HOgz1BeC?4ImmoG=OLT(Ey?WL<5Kh5Dg$2Ks1180MP)V0Yn3c1`rJ(8bCCF zXaLawq5(t$hz1Z1AR0h4fM@{G0HOgz1BeC?4ImmoG=OLT(Ey?WL<5Kh5Dg$2Ks118 z0MP)V0Yn3c1`rJ(8bCCFXaLawq5(t$hz1Z1AR0h4fM@{G0HOgz1BeC?4ImmoG=OLT z(Ey?WL<5Kh5Dg$2Ks1180MP)V0Yn3c1`rJ(8bCCFXaLawq5(t$hz1Z1AR0h4fM@{G z0HOgz1BeC?4ImmoG=OLT(Ey?WL<5Kh5Dg$2Ks1180MP)V0Yn3c1`rJ(8bCCFXaLaw zq5(t$hz1Z1AR0h4fM@{G0HOgz1BeC?4ImmoG=OLT(Ey?WL<5Kh5Dg$2Ks1180MP)V z0Yn3c1`rJ(8bCCFXaLawq5(t$hz1Z1AR0h4fM@{G0HOgz1BeC?4ImmoG=OLT(Ey?W zL<5Kh5Dg$2Ks1180MP)V0Yn3c1`rJ(8bCCFXaLawq5(t$hz1Z1AR0h4fM@{G0HOgz z1BeC?4ImmoG=OLT(Ey?WL<5Kh5Dg$2Ks1180MP)V0Yn3c1`rJ(8bCCFXaLawq5(t$ zhz1Z1AR0h4fM@{G0HOgz1BeC?4ImmoG=OLT(Ey?WL<5Kh5Dg$2Ks1180MP)V0Yn3c z1`rJ(8bCCFXaLawq5(t$hz1Z1AR0h4fM@{G0HOgz1BeC?4ImmoG=OLT(Ey?WL<5Kh z5Dg$2Ks1180MP)V0Yn3c1`rJ(8bCCFXaLawq5(t$hz1Z1AR0h4fM@{G0HOgz1BeC? z4ImmoG=OLT(Ey?WL<5Kh5Dg$2Ks1180MP)V0Yn3c1`rJ(8bCCFXaLawq5(t$hz1Z1 zAR0h4fM@{G0HOgz1BeC?4ImmoG=OLT(Ey?WL<5Kh5Dg$2Ks1180MP)V0Yn3c1`rJ( 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 V(#i@ZjE8THnAT(!gb2pG_5ZYwKd}G+ literal 0 HcmV?d00001 From d3903f885a8fde9f19dddb002dc78ba3686c01c5 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Fri, 14 Apr 2023 14:48:21 -0700 Subject: [PATCH 11/30] Update Flatbuffers to 23.1.21 PiperOrigin-RevId: 524391723 --- third_party/flatbuffers/BUILD.bazel | 4 ++++ third_party/flatbuffers/workspace.bzl | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/third_party/flatbuffers/BUILD.bazel b/third_party/flatbuffers/BUILD.bazel index 53cecc73..d5264a02 100644 --- a/third_party/flatbuffers/BUILD.bazel +++ b/third_party/flatbuffers/BUILD.bazel @@ -45,12 +45,16 @@ filegroup( "include/flatbuffers/bfbs_generator.h", "include/flatbuffers/buffer.h", "include/flatbuffers/buffer_ref.h", + "include/flatbuffers/code_generator.h", "include/flatbuffers/code_generators.h", "include/flatbuffers/default_allocator.h", "include/flatbuffers/detached_buffer.h", "include/flatbuffers/flatbuffer_builder.h", "include/flatbuffers/flatbuffers.h", + "include/flatbuffers/flatc.h", + "include/flatbuffers/flex_flat_util.h", "include/flatbuffers/flexbuffers.h", + "include/flatbuffers/grpc.h", "include/flatbuffers/hash.h", "include/flatbuffers/idl.h", "include/flatbuffers/minireflect.h", diff --git a/third_party/flatbuffers/workspace.bzl b/third_party/flatbuffers/workspace.bzl index 703cb053..02247268 100644 --- a/third_party/flatbuffers/workspace.bzl +++ b/third_party/flatbuffers/workspace.bzl @@ -5,11 +5,11 @@ load("//third_party:repo.bzl", "third_party_http_archive") def repo(): third_party_http_archive( name = "flatbuffers", - strip_prefix = "flatbuffers-2.0.6", - sha256 = "e2dc24985a85b278dd06313481a9ca051d048f9474e0f199e372fea3ea4248c9", + strip_prefix = "flatbuffers-23.1.21", + sha256 = "d84cb25686514348e615163b458ae0767001b24b42325f426fd56406fd384238", urls = [ - "https://storage.googleapis.com/mirror.tensorflow.org/github.com/google/flatbuffers/archive/v2.0.6.tar.gz", - "https://github.com/google/flatbuffers/archive/v2.0.6.tar.gz", + "https://storage.googleapis.com/mirror.tensorflow.org/github.com/google/flatbuffers/archive/v23.1.21.tar.gz", + "https://github.com/google/flatbuffers/archive/v23.1.21.tar.gz", ], build_file = "//third_party/flatbuffers:BUILD.bazel", delete = ["build_defs.bzl", "BUILD.bazel"], From 67ed42d9c6ceb531f1ca1eed9cb8e83812902251 Mon Sep 17 00:00:00 2001 From: Jiuqiang Tang Date: Fri, 14 Apr 2023 15:12:08 -0700 Subject: [PATCH 12/30] Always link the "pose_landmarks_detector_graph" target. PiperOrigin-RevId: 524397174 --- mediapipe/tasks/cc/vision/pose_landmarker/BUILD | 1 + 1 file changed, 1 insertion(+) diff --git a/mediapipe/tasks/cc/vision/pose_landmarker/BUILD b/mediapipe/tasks/cc/vision/pose_landmarker/BUILD index 07cf793e..19f54625 100644 --- a/mediapipe/tasks/cc/vision/pose_landmarker/BUILD +++ b/mediapipe/tasks/cc/vision/pose_landmarker/BUILD @@ -100,6 +100,7 @@ cc_library( "//mediapipe/util:graph_builder_utils", "@com_google_absl//absl/status:statusor", ], + alwayslink = 1, ) cc_library( From e1643a5abb7bc91ff00118636e4a888e95754c27 Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Fri, 14 Apr 2023 16:38:19 -0700 Subject: [PATCH 13/30] Fix in preparation for Emscripten updates PiperOrigin-RevId: 524414804 --- mediapipe/gpu/gl_context_webgl.cc | 53 ++++--------------------------- 1 file changed, 7 insertions(+), 46 deletions(-) diff --git a/mediapipe/gpu/gl_context_webgl.cc b/mediapipe/gpu/gl_context_webgl.cc index b1f5295c..25cbed83 100644 --- a/mediapipe/gpu/gl_context_webgl.cc +++ b/mediapipe/gpu/gl_context_webgl.cc @@ -67,53 +67,14 @@ absl::Status GlContext::CreateContextInternal( // TODO: Investigate this option in more detail, esp. on Safari. attrs.preserveDrawingBuffer = 0; - // Since the Emscripten canvas target finding function is visible from here, - // we hijack findCanvasEventTarget directly for enforcing old Module.canvas - // behavior if the user desires, falling back to the new DOM element CSS - // selector behavior next if that is specified, and finally just allowing the - // lookup to proceed on a null target. - // TODO: Ensure this works with all options (in particular, - // multithreading options, like the special-case combination of USE_PTHREADS - // and OFFSCREEN_FRAMEBUFFER) - // clang-format off - EM_ASM( - let init_once = true; - if (init_once) { - const cachedFindCanvasEventTarget = findCanvasEventTarget; - - if (typeof cachedFindCanvasEventTarget !== 'function') { - if (typeof console !== 'undefined') { - console.error('Expected Emscripten global function ' - + '"findCanvasEventTarget" not found. WebGL context creation ' - + 'may fail.'); - } - return; - } - - findCanvasEventTarget = function(target) { - if (target == 0) { - if (Module && Module.canvas) { - return Module.canvas; - } else if (Module && Module.canvasCssSelector) { - return cachedFindCanvasEventTarget(Module.canvasCssSelector); - } - if (typeof console !== 'undefined') { - console.warn('Module properties canvas and canvasCssSelector not ' + - 'found during WebGL context creation.'); - } - } - // We still go through with the find attempt, although for most use - // cases it will not succeed, just in case the user does want to fall- - // back. - return cachedFindCanvasEventTarget(target); - }; // NOLINT: Necessary semicolon. - init_once = false; - } - ); - // clang-format on - + // Quick patch for -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR so it also + // looks for our #canvas target in Module.canvas, where we expect it to be. + // -s OFFSCREENCANVAS_SUPPORT=1 will no longer work with this under the new + // event target behavior, but it was never supposed to be tapping into our + // canvas anyways. See b/278155946 for more background. + EM_ASM({ specialHTMLTargets["#canvas"] = Module.canvas; }); EMSCRIPTEN_WEBGL_CONTEXT_HANDLE context_handle = - emscripten_webgl_create_context(nullptr, &attrs); + emscripten_webgl_create_context("#canvas", &attrs); // Check for failure if (context_handle <= 0) { From e14a88052aa83bb26ccf5915ecc9bad7b32a5273 Mon Sep 17 00:00:00 2001 From: Jiuqiang Tang Date: Fri, 14 Apr 2023 19:35:58 -0700 Subject: [PATCH 14/30] Remove resizing and rotation from face stylizer's postprocessing step. The resizing and rotation logic is useful when the pipeline needs to paste the stylized face image back to the original image. To only output the stylized face image, resizing and rotation may result to strange outputs. PiperOrigin-RevId: 524441372 --- mediapipe/tasks/cc/vision/face_stylizer/BUILD | 9 +-- .../cc/vision/face_stylizer/face_stylizer.h | 14 ++--- .../face_stylizer/face_stylizer_graph.cc | 55 ++--------------- .../vision/facestylizer/FaceStylizer.java | 60 +++++++++---------- .../vision/facestylizer/FaceStylizerTest.java | 4 +- .../tasks/python/vision/face_stylizer.py | 21 ++----- .../web/vision/face_stylizer/face_stylizer.ts | 24 ++------ 7 files changed, 55 insertions(+), 132 deletions(-) diff --git a/mediapipe/tasks/cc/vision/face_stylizer/BUILD b/mediapipe/tasks/cc/vision/face_stylizer/BUILD index bdbf340b..27b2f482 100644 --- a/mediapipe/tasks/cc/vision/face_stylizer/BUILD +++ b/mediapipe/tasks/cc/vision/face_stylizer/BUILD @@ -23,18 +23,12 @@ cc_library( srcs = ["face_stylizer_graph.cc"], deps = [ "//mediapipe/calculators/core:split_vector_calculator_cc_proto", - "//mediapipe/calculators/image:image_cropping_calculator", - "//mediapipe/calculators/image:image_cropping_calculator_cc_proto", - "//mediapipe/calculators/image:warp_affine_calculator", - "//mediapipe/calculators/image:warp_affine_calculator_cc_proto", + "//mediapipe/calculators/image:image_clone_calculator_cc_proto", "//mediapipe/calculators/tensor:image_to_tensor_calculator_cc_proto", "//mediapipe/calculators/tensor:inference_calculator", "//mediapipe/calculators/util:detections_to_rects_calculator", "//mediapipe/calculators/util:face_to_rect_calculator", - "//mediapipe/calculators/util:from_image_calculator", - "//mediapipe/calculators/util:inverse_matrix_calculator", "//mediapipe/calculators/util:landmarks_to_detection_calculator_cc_proto", - "//mediapipe/calculators/util:to_image_calculator", "//mediapipe/framework/api2:builder", "//mediapipe/framework/api2:port", "//mediapipe/framework/formats:image", @@ -53,7 +47,6 @@ cc_library( "//mediapipe/tasks/cc/vision/face_landmarker:face_landmarker_graph", "//mediapipe/tasks/cc/vision/face_landmarker/proto:face_landmarker_graph_options_cc_proto", "//mediapipe/tasks/cc/vision/face_landmarker/proto:face_landmarks_detector_graph_options_cc_proto", - "//mediapipe/tasks/cc/vision/face_stylizer/calculators:strip_rotation_calculator", "//mediapipe/tasks/cc/vision/face_stylizer/calculators:tensors_to_image_calculator", "//mediapipe/tasks/cc/vision/face_stylizer/calculators:tensors_to_image_calculator_cc_proto", "//mediapipe/tasks/cc/vision/face_stylizer/proto:face_stylizer_graph_options_cc_proto", diff --git a/mediapipe/tasks/cc/vision/face_stylizer/face_stylizer.h b/mediapipe/tasks/cc/vision/face_stylizer/face_stylizer.h index 14c23b7a..36bb11bd 100644 --- a/mediapipe/tasks/cc/vision/face_stylizer/face_stylizer.h +++ b/mediapipe/tasks/cc/vision/face_stylizer/face_stylizer.h @@ -84,9 +84,7 @@ class FaceStylizer : tasks::vision::core::BaseVisionTaskApi { // The input image can be of any size with format RGB or RGBA. // When no face is detected on the input image, the method returns a // std::nullopt. Otherwise, returns the stylized image of the most visible - // face. To ensure that the output image has reasonable quality, the stylized - // output image size is the smaller of the model output size and the size of - // the 'region_of_interest' specified in 'image_processing_options'. + // face. The stylized output image size is the same as the model output size. absl::StatusOr> Stylize( mediapipe::Image image, std::optional image_processing_options = @@ -111,9 +109,7 @@ class FaceStylizer : tasks::vision::core::BaseVisionTaskApi { // must be monotonically increasing. // When no face is detected on the input image, the method returns a // std::nullopt. Otherwise, returns the stylized image of the most visible - // face. To ensure that the output image has reasonable quality, the stylized - // output image size is the smaller of the model output size and the size of - // the 'region_of_interest' specified in 'image_processing_options'. + // face. The stylized output image size is the same as the model output size. absl::StatusOr> StylizeForVideo( mediapipe::Image image, int64_t timestamp_ms, std::optional image_processing_options = @@ -143,10 +139,8 @@ class FaceStylizer : tasks::vision::core::BaseVisionTaskApi { // The "result_callback" provides: // - When no face is detected on the input image, the method returns a // std::nullopt. Otherwise, returns the stylized image of the most visible - // face. To ensure that the output image has reasonable quality, the - // stylized output image size is the smaller of the model output size and - // the size of the 'region_of_interest' specified in - // 'image_processing_options'. + // face. The stylized output image size is the same as the model output + // size. // - The input timestamp in milliseconds. absl::Status StylizeAsync(mediapipe::Image image, int64_t timestamp_ms, std::optional diff --git a/mediapipe/tasks/cc/vision/face_stylizer/face_stylizer_graph.cc b/mediapipe/tasks/cc/vision/face_stylizer/face_stylizer_graph.cc index bf717a71..27b8dacc 100644 --- a/mediapipe/tasks/cc/vision/face_stylizer/face_stylizer_graph.cc +++ b/mediapipe/tasks/cc/vision/face_stylizer/face_stylizer_graph.cc @@ -19,8 +19,7 @@ limitations under the License. #include "absl/memory/memory.h" #include "absl/status/statusor.h" #include "mediapipe/calculators/core/split_vector_calculator.pb.h" -#include "mediapipe/calculators/image/image_cropping_calculator.pb.h" -#include "mediapipe/calculators/image/warp_affine_calculator.pb.h" +#include "mediapipe/calculators/image/image_clone_calculator.pb.h" #include "mediapipe/calculators/tensor/image_to_tensor_calculator.pb.h" #include "mediapipe/calculators/util/landmarks_to_detection_calculator.pb.h" #include "mediapipe/framework/api2/builder.h" @@ -326,7 +325,6 @@ class FaceStylizerGraph : public core::ModelTaskGraph { image_in >> preprocessing.In(kImageTag); face_rect >> preprocessing.In(kNormRectTag); auto preprocessed_tensors = preprocessing.Out(kTensorsTag); - auto transform_matrix = preprocessing.Out(kMatrixTag); // Adds inference subgraph and connects its input stream to the output // tensors produced by the ImageToTensorCalculator. @@ -344,53 +342,12 @@ class FaceStylizerGraph : public core::ModelTaskGraph { model_output_tensors >> tensors_to_image.In(kTensorsTag); auto tensor_image = tensors_to_image.Out(kImageTag); - auto& inverse_matrix = graph.AddNode("InverseMatrixCalculator"); - transform_matrix >> inverse_matrix.In(kMatrixTag); - auto inverse_transform_matrix = inverse_matrix.Out(kMatrixTag); + auto& image_converter = graph.AddNode("ImageCloneCalculator"); + image_converter.GetOptions() + .set_output_on_gpu(false); + tensor_image >> image_converter.In(""); - auto& warp_affine = graph.AddNode("WarpAffineCalculator"); - auto& warp_affine_options = - warp_affine.GetOptions(); - warp_affine_options.set_border_mode( - WarpAffineCalculatorOptions::BORDER_ZERO); - warp_affine_options.set_gpu_origin(mediapipe::GpuOrigin_Mode_TOP_LEFT); - tensor_image >> warp_affine.In(kImageTag); - inverse_transform_matrix >> warp_affine.In(kMatrixTag); - image_size >> warp_affine.In(kOutputSizeTag); - auto image_to_crop = warp_affine.Out(kImageTag); - - // The following calculators are for cropping and resizing the output image - // based on the roi and the model output size. As the WarpAffineCalculator - // rotates the image based on the transform matrix, the rotation info in the - // rect proto is stripped to prevent the ImageCroppingCalculator from - // performing extra rotation. - auto& strip_rotation = - graph.AddNode("mediapipe.tasks.StripRotationCalculator"); - face_rect >> strip_rotation.In(kNormRectTag); - auto norm_rect_no_rotation = strip_rotation.Out(kNormRectTag); - auto& from_image = graph.AddNode("FromImageCalculator"); - image_to_crop >> from_image.In(kImageTag); - auto& image_cropping = graph.AddNode("ImageCroppingCalculator"); - auto& image_cropping_opts = - image_cropping.GetOptions(); - image_cropping_opts.set_output_max_width( - image_to_tensor_options.output_tensor_width()); - image_cropping_opts.set_output_max_height( - image_to_tensor_options.output_tensor_height()); - norm_rect_no_rotation >> image_cropping.In(kNormRectTag); - auto& to_image = graph.AddNode("ToImageCalculator"); - // ImageCroppingCalculator currently doesn't support mediapipe::Image, the - // graph selects its cpu or gpu path based on the image preprocessing - // backend. - if (use_gpu) { - from_image.Out(kImageGpuTag) >> image_cropping.In(kImageGpuTag); - image_cropping.Out(kImageGpuTag) >> to_image.In(kImageGpuTag); - } else { - from_image.Out(kImageCpuTag) >> image_cropping.In(kImageTag); - image_cropping.Out(kImageTag) >> to_image.In(kImageCpuTag); - } - - return {{/*stylized_image=*/to_image.Out(kImageTag).Cast(), + return {{/*stylized_image=*/image_converter.Out("").Cast(), /*original_image=*/preprocessing.Out(kImageTag).Cast()}}; } }; diff --git a/mediapipe/tasks/java/com/google/mediapipe/tasks/vision/facestylizer/FaceStylizer.java b/mediapipe/tasks/java/com/google/mediapipe/tasks/vision/facestylizer/FaceStylizer.java index b95e9021..d6f565c7 100644 --- a/mediapipe/tasks/java/com/google/mediapipe/tasks/vision/facestylizer/FaceStylizer.java +++ b/mediapipe/tasks/java/com/google/mediapipe/tasks/vision/facestylizer/FaceStylizer.java @@ -198,9 +198,9 @@ public final class FaceStylizer extends BaseVisionTaskApi { *
  • {@link android.graphics.Bitmap.Config#ARGB_8888} * * - *

    The image can be of any size. To ensure that the output image has reasonable quality, the - * size of the stylized output is based the model output size and can be smaller than the input - * image. + *

    The input image can be of any size. The output image is the stylized image with the most + * visible face. The stylized output image size is the same as the model output size. When no face + * is detected on the input image, returns {@code Optional.empty()}. * * @param image a MediaPipe {@link MPImage} object for processing. * @throws MediaPipeException if there is an internal error. Or if {@link FaceStylizer} is created @@ -220,9 +220,9 @@ public final class FaceStylizer extends BaseVisionTaskApi { *

  • {@link android.graphics.Bitmap.Config#ARGB_8888} * * - *

    The input image can be of any size. To ensure that the output image has reasonable quality, - * the stylized output image size is the smaller of the model output size and the size of the - * {@link ImageProcessingOptions#regionOfInterest} specified in {@code imageProcessingOptions}. + *

    The input image can be of any size. The output image is the stylized image with the most + * visible face. The stylized output image size is the same as the model output size. When no face + * is detected on the input image, returns {@code Optional.empty()}. * * @param image a MediaPipe {@link MPImage} object for processing. * @param imageProcessingOptions the {@link ImageProcessingOptions} specifying how to process the @@ -256,9 +256,9 @@ public final class FaceStylizer extends BaseVisionTaskApi { *

  • {@link android.graphics.Bitmap.Config#ARGB_8888} * * - *

    The image can be of any size. To ensure that the output image has reasonable quality, the - * size of the stylized output is based the model output size and can be smaller than the input - * image. + *

    The input image can be of any size. The output image is the stylized image with the most + * visible face. The stylized output image size is the same as the model output size. When no face + * is detected on the input image, returns {@code Optional.empty()}. * * @param image a MediaPipe {@link MPImage} object for processing. * @throws IllegalArgumentException if the {@link ImageProcessingOptions} specify a @@ -281,9 +281,9 @@ public final class FaceStylizer extends BaseVisionTaskApi { *

  • {@link android.graphics.Bitmap.Config#ARGB_8888} * * - *

    The input image can be of any size. To ensure that the output image has reasonable quality, - * the stylized output image size is the smaller of the model output size and the size of the - * {@link ImageProcessingOptions#regionOfInterest} specified in {@code imageProcessingOptions}. + *

    The input image can be of any size. The output image is the stylized image with the most + * visible face. The stylized output image size is the same as the model output size. When no face + * is detected on the input image, returns {@code Optional.empty()}. * * @param image a MediaPipe {@link MPImage} object for processing. * @param imageProcessingOptions the {@link ImageProcessingOptions} specifying how to process the @@ -320,9 +320,9 @@ public final class FaceStylizer extends BaseVisionTaskApi { *

  • {@link android.graphics.Bitmap.Config#ARGB_8888} * * - *

    The image can be of any size. To ensure that the output image has reasonable quality, the - * size of the stylized output is based the model output size and can be smaller than the input - * image. + *

    The input image can be of any size. The output image is the stylized image with the most + * visible face. The stylized output image size is the same as the model output size. When no face + * is detected on the input image, returns {@code Optional.empty()}. * * @param image a MediaPipe {@link MPImage} object for processing. * @param timestampMs the input timestamp (in milliseconds). @@ -346,9 +346,9 @@ public final class FaceStylizer extends BaseVisionTaskApi { *

  • {@link android.graphics.Bitmap.Config#ARGB_8888} * * - *

    The input image can be of any size. To ensure that the output image has reasonable quality, - * the stylized output image size is the smaller of the model output size and the size of the - * {@link ImageProcessingOptions#regionOfInterest} specified in {@code imageProcessingOptions}. + *

    The input image can be of any size. The output image is the stylized image with the most + * visible face. The stylized output image size is the same as the model output size. When no face + * is detected on the input image, returns {@code Optional.empty()}. * * * @param image a MediaPipe {@link MPImage} object for processing. * @param imageProcessingOptions the {@link ImageProcessingOptions} specifying how to process the @@ -387,9 +387,9 @@ public final class FaceStylizer extends BaseVisionTaskApi { *

  • {@link android.graphics.Bitmap.Config#ARGB_8888} * * - *

    The image can be of any size. To ensure that the output image has reasonable quality, the - * size of the stylized output is based the model output size and can be smaller than the input - * image. + *

    The input image can be of any size. The output image is the stylized image with the most + * visible face. The stylized output image size is the same as the model output size. When no face + * is detected on the input image, returns {@code Optional.empty()}. * * @param image a MediaPipe {@link MPImage} object for processing. * @param timestampMs the input timestamp (in milliseconds). @@ -414,9 +414,9 @@ public final class FaceStylizer extends BaseVisionTaskApi { *

  • {@link android.graphics.Bitmap.Config#ARGB_8888} * * - *

    The input image can be of any size. To ensure that the output image has reasonable quality, - * the stylized output image size is the smaller of the model output size and the size of the - * {@link ImageProcessingOptions#regionOfInterest} specified in {@code imageProcessingOptions}. + *

    The input image can be of any size. The output image is the stylized image with the most + * visible face. The stylized output image size is the same as the model output size. When no face + * is detected on the input image, returns {@code Optional.empty()}. * * @param image a MediaPipe {@link MPImage} object for processing. * @param timestampMs the input timestamp (in milliseconds). @@ -445,9 +445,9 @@ public final class FaceStylizer extends BaseVisionTaskApi { * *

    {@link FaceStylizer} supports the following color space types: * - *

    The image can be of any size. To ensure that the output image has reasonable quality, the - * size of the stylized output is based the model output * size and can be smaller than the input - * image. + *

    The input image can be of any size. The output image is the stylized image with the most + * visible face. The stylized output image size is the same as the model output size. When no face + * is detected on the input image, returns {@code Optional.empty()}. * *

      *
    • {@link android.graphics.Bitmap.Config#ARGB_8888} @@ -475,9 +475,9 @@ public final class FaceStylizer extends BaseVisionTaskApi { *
    • {@link android.graphics.Bitmap.Config#ARGB_8888} *
    * - *

    The input image can be of any size. To ensure that the output image has reasonable quality, - * the stylized output image size is the smaller of the model output size and the size of the - * {@link ImageProcessingOptions#regionOfInterest} specified in {@code imageProcessingOptions}. + *

    The input image can be of any size. The output image is the stylized image with the most + * visible face. The stylized output image size is the same as the model output size. When no face + * is detected on the input image, returns {@code Optional.empty()}. * * @param image a MediaPipe {@link MPImage} object for processing. * @param imageProcessingOptions the {@link ImageProcessingOptions} specifying how to process the diff --git a/mediapipe/tasks/javatests/com/google/mediapipe/tasks/vision/facestylizer/FaceStylizerTest.java b/mediapipe/tasks/javatests/com/google/mediapipe/tasks/vision/facestylizer/FaceStylizerTest.java index 5b880f41..4f6cc2d6 100644 --- a/mediapipe/tasks/javatests/com/google/mediapipe/tasks/vision/facestylizer/FaceStylizerTest.java +++ b/mediapipe/tasks/javatests/com/google/mediapipe/tasks/vision/facestylizer/FaceStylizerTest.java @@ -234,8 +234,8 @@ public class FaceStylizerTest { FaceStylizerResult actualResult = faceStylizer.stylize(inputImage); MPImage stylizedImage = actualResult.stylizedImage().get(); assertThat(stylizedImage).isNotNull(); - assertThat(stylizedImage.getWidth()).isEqualTo(83); - assertThat(stylizedImage.getHeight()).isEqualTo(83); + assertThat(stylizedImage.getWidth()).isEqualTo(modelImageSize); + assertThat(stylizedImage.getHeight()).isEqualTo(modelImageSize); } @Test diff --git a/mediapipe/tasks/python/vision/face_stylizer.py b/mediapipe/tasks/python/vision/face_stylizer.py index c6470b19..0b10a2b4 100644 --- a/mediapipe/tasks/python/vision/face_stylizer.py +++ b/mediapipe/tasks/python/vision/face_stylizer.py @@ -176,16 +176,13 @@ class FaceStylizer(base_vision_task_api.BaseVisionTaskApi): Only use this method when the FaceStylizer is created with the image running mode. - To ensure that the output image has reasonable quality, the stylized output - image size is the smaller of the model output size and the size of the - `region_of_interest` specified in `image_processing_options`. - Args: image: MediaPipe Image. image_processing_options: Options for image processing. Returns: - The stylized image of the most visible face. None if no face is detected + The stylized image of the most visible face. The stylized output image + size is the same as the model output size. None if no face is detected on the input image. Raises: @@ -217,17 +214,14 @@ class FaceStylizer(base_vision_task_api.BaseVisionTaskApi): milliseconds) along with the video frame. The input timestamps should be monotonically increasing for adjacent calls of this method. - To ensure that the output image has reasonable quality, the stylized output - image size is the smaller of the model output size and the size of the - `region_of_interest` specified in `image_processing_options`. - Args: image: MediaPipe Image. timestamp_ms: The timestamp of the input video frame in milliseconds. image_processing_options: Options for image processing. Returns: - The stylized image of the most visible face. None if no face is detected + The stylized image of the most visible face. The stylized output image + size is the same as the model output size. None if no face is detected on the input image. Raises: @@ -266,12 +260,9 @@ class FaceStylizer(base_vision_task_api.BaseVisionTaskApi): images if needed. In other words, it's not guaranteed to have output per input image. - To ensure that the stylized image has reasonable quality, the stylized - output image size is the smaller of the model output size and the size of - the `region_of_interest` specified in `image_processing_options`. - The `result_callback` provides: - - The stylized image of the most visible face. None if no face is detected + - The stylized image of the most visible face. The stylized output image + size is the same as the model output size. None if no face is detected on the input image. - The input image that the face stylizer runs on. - The input timestamp in milliseconds. diff --git a/mediapipe/tasks/web/vision/face_stylizer/face_stylizer.ts b/mediapipe/tasks/web/vision/face_stylizer/face_stylizer.ts index 34067aab..dfce0303 100644 --- a/mediapipe/tasks/web/vision/face_stylizer/face_stylizer.ts +++ b/mediapipe/tasks/web/vision/face_stylizer/face_stylizer.ts @@ -129,10 +129,6 @@ export class FaceStylizer extends VisionTaskRunner { * synchronously once the callback returns. Only use this method when the * FaceStylizer is created with the image running mode. * - * The input image can be of any size. To ensure that the output image has - * reasonable quality, the stylized output image size is determined by the - * model output size. - * * @param image An image to process. * @param callback The callback that is invoked with the stylized image. The * lifetime of the returned data is only guaranteed for the duration of the @@ -153,11 +149,6 @@ export class FaceStylizer extends VisionTaskRunner { * If both are specified, the crop around the region-of-interest is extracted * first, then the specified rotation is applied to the crop. * - * The input image can be of any size. To ensure that the output image has - * reasonable quality, the stylized output image size is the smaller of the - * model output size and the size of the 'regionOfInterest' specified in - * 'imageProcessingOptions'. - * * @param image An image to process. * @param imageProcessingOptions the `ImageProcessingOptions` specifying how * to process the input image before running inference. @@ -192,9 +183,6 @@ export class FaceStylizer extends VisionTaskRunner { * frame's timestamp (in milliseconds). The input timestamps must be * monotonically increasing. * - * To ensure that the output image has reasonable quality, the stylized - * output image size is determined by the model output size. - * * @param videoFrame A video frame to process. * @param timestamp The timestamp of the current frame, in ms. * @param callback The callback that is invoked with the stylized image. The @@ -221,10 +209,6 @@ export class FaceStylizer extends VisionTaskRunner { * frame's timestamp (in milliseconds). The input timestamps must be * monotonically increasing. * - * To ensure that the output image has reasonable quality, the stylized - * output image size is the smaller of the model output size and the size of - * the 'regionOfInterest' specified in 'imageProcessingOptions'. - * * @param videoFrame A video frame to process. * @param imageProcessingOptions the `ImageProcessingOptions` specifying how * to process the input image before running inference. @@ -278,8 +262,12 @@ export class FaceStylizer extends VisionTaskRunner { this.graphRunner.attachImageListener( STYLIZED_IMAGE_STREAM, (image, timestamp) => { - const imageData = this.convertToImageData(image); - this.userCallback(imageData, image.width, image.height); + if (image.data instanceof WebGLTexture) { + this.userCallback(image.data, image.width, image.height); + } else { + const imageData = this.convertToImageData(image); + this.userCallback(imageData, image.width, image.height); + } this.setLatestOutputTimestamp(timestamp); }); this.graphRunner.attachEmptyPacketListener( From 411ffaeb4389b99cdf146dd0ad60f80e12b31048 Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Fri, 14 Apr 2023 19:43:04 -0700 Subject: [PATCH 15/30] Update Java interactive segmenter to output both confidence masks and category mask optionally. PiperOrigin-RevId: 524442070 --- .../InteractiveSegmenter.java | 149 ++++++++++-------- .../InteractiveSegmenterTest.java | 17 +- 2 files changed, 90 insertions(+), 76 deletions(-) diff --git a/mediapipe/tasks/java/com/google/mediapipe/tasks/vision/interactivesegmenter/InteractiveSegmenter.java b/mediapipe/tasks/java/com/google/mediapipe/tasks/vision/interactivesegmenter/InteractiveSegmenter.java index b38bd1c8..6f47797b 100644 --- a/mediapipe/tasks/java/com/google/mediapipe/tasks/vision/interactivesegmenter/InteractiveSegmenter.java +++ b/mediapipe/tasks/java/com/google/mediapipe/tasks/vision/interactivesegmenter/InteractiveSegmenter.java @@ -94,15 +94,7 @@ public final class InteractiveSegmenter extends BaseVisionTaskApi { "IMAGE:" + IMAGE_IN_STREAM_NAME, "ROI:" + ROI_IN_STREAM_NAME, "NORM_RECT:" + NORM_RECT_IN_STREAM_NAME)); - private static final List OUTPUT_STREAMS = - Collections.unmodifiableList( - Arrays.asList( - "GROUPED_SEGMENTATION:segmented_mask_out", - "IMAGE:image_out", - "SEGMENTATION:0:segmentation")); - private static final int GROUPED_SEGMENTATION_OUT_STREAM_INDEX = 0; - private static final int IMAGE_OUT_STREAM_INDEX = 1; - private static final int SEGMENTATION_OUT_STREAM_INDEX = 2; + private static final int IMAGE_OUT_STREAM_INDEX = 0; private static final String TASK_GRAPH_NAME = "mediapipe.tasks.vision.interactive_segmenter.InteractiveSegmenterGraph"; private static final String TENSORS_TO_SEGMENTATION_CALCULATOR_NAME = @@ -123,6 +115,21 @@ public final class InteractiveSegmenter extends BaseVisionTaskApi { */ public static InteractiveSegmenter createFromOptions( Context context, InteractiveSegmenterOptions segmenterOptions) { + if (!segmenterOptions.outputConfidenceMasks() && !segmenterOptions.outputCategoryMask()) { + throw new IllegalArgumentException( + "At least one of `outputConfidenceMasks` and `outputCategoryMask` must be set."); + } + List outputStreams = new ArrayList<>(); + outputStreams.add("IMAGE:image_out"); + if (segmenterOptions.outputConfidenceMasks()) { + outputStreams.add("CONFIDENCE_MASKS:confidence_masks"); + } + final int confidenceMasksOutStreamIndex = outputStreams.size() - 1; + if (segmenterOptions.outputCategoryMask()) { + outputStreams.add("CATEGORY_MASK:category_mask"); + } + final int categoryMaskOutStreamIndex = outputStreams.size() - 1; + // TODO: Consolidate OutputHandler and TaskRunner. OutputHandler handler = new OutputHandler<>(); handler.setOutputPacketConverter( @@ -130,52 +137,72 @@ public final class InteractiveSegmenter extends BaseVisionTaskApi { @Override public ImageSegmenterResult convertToTaskResult(List packets) throws MediaPipeException { - if (packets.get(GROUPED_SEGMENTATION_OUT_STREAM_INDEX).isEmpty()) { + if (packets.get(IMAGE_OUT_STREAM_INDEX).isEmpty()) { return ImageSegmenterResult.create( Optional.empty(), Optional.empty(), - packets.get(GROUPED_SEGMENTATION_OUT_STREAM_INDEX).getTimestamp()); + packets.get(IMAGE_OUT_STREAM_INDEX).getTimestamp()); } - List segmentedMasks = new ArrayList<>(); - int width = PacketGetter.getImageWidth(packets.get(SEGMENTATION_OUT_STREAM_INDEX)); - int height = PacketGetter.getImageHeight(packets.get(SEGMENTATION_OUT_STREAM_INDEX)); - int imageFormat = - segmenterOptions.outputType() - == InteractiveSegmenterOptions.OutputType.CONFIDENCE_MASK - ? MPImage.IMAGE_FORMAT_VEC32F1 - : MPImage.IMAGE_FORMAT_ALPHA; - int imageListSize = - PacketGetter.getImageListSize(packets.get(GROUPED_SEGMENTATION_OUT_STREAM_INDEX)); - ByteBuffer[] buffersArray = new ByteBuffer[imageListSize]; - // If resultListener is not provided, the resulted MPImage is deep copied from mediapipe - // graph. If provided, the result MPImage is wrapping the mediapipe packet memory. - if (!segmenterOptions.resultListener().isPresent()) { - for (int i = 0; i < imageListSize; i++) { - buffersArray[i] = - ByteBuffer.allocateDirect( - width * height * (imageFormat == MPImage.IMAGE_FORMAT_VEC32F1 ? 4 : 1)); + // If resultListener is not provided, the resulted MPImage is deep copied from + // mediapipe graph. If provided, the result MPImage is wrapping the mediapipe packet + // memory. + boolean copyImage = !segmenterOptions.resultListener().isPresent(); + Optional> confidenceMasks = Optional.empty(); + if (segmenterOptions.outputConfidenceMasks()) { + confidenceMasks = Optional.of(new ArrayList<>()); + int width = + PacketGetter.getImageWidthFromImageList( + packets.get(confidenceMasksOutStreamIndex)); + int height = + PacketGetter.getImageHeightFromImageList( + packets.get(confidenceMasksOutStreamIndex)); + int imageListSize = + PacketGetter.getImageListSize(packets.get(confidenceMasksOutStreamIndex)); + ByteBuffer[] buffersArray = new ByteBuffer[imageListSize]; + // confidence masks are float type image. + final int numBytes = 4; + if (copyImage) { + for (int i = 0; i < imageListSize; i++) { + buffersArray[i] = ByteBuffer.allocateDirect(width * height * numBytes); + } + } + if (!PacketGetter.getImageList( + packets.get(confidenceMasksOutStreamIndex), buffersArray, copyImage)) { + throw new MediaPipeException( + MediaPipeException.StatusCode.INTERNAL.ordinal(), + "There is an error getting confidence masks."); + } + for (ByteBuffer buffer : buffersArray) { + ByteBufferImageBuilder builder = + new ByteBufferImageBuilder(buffer, width, height, MPImage.IMAGE_FORMAT_VEC32F1); + confidenceMasks.get().add(builder.build()); } } - if (!PacketGetter.getImageList( - packets.get(GROUPED_SEGMENTATION_OUT_STREAM_INDEX), - buffersArray, - !segmenterOptions.resultListener().isPresent())) { - throw new MediaPipeException( - MediaPipeException.StatusCode.INTERNAL.ordinal(), - "There is an error getting segmented masks. It usually results from incorrect" - + " options of unsupported OutputType of given model."); - } - for (ByteBuffer buffer : buffersArray) { + Optional categoryMask = Optional.empty(); + if (segmenterOptions.outputCategoryMask()) { + int width = PacketGetter.getImageWidth(packets.get(categoryMaskOutStreamIndex)); + int height = PacketGetter.getImageHeight(packets.get(categoryMaskOutStreamIndex)); + ByteBuffer buffer; + if (copyImage) { + buffer = ByteBuffer.allocateDirect(width * height); + if (!PacketGetter.getImageData(packets.get(categoryMaskOutStreamIndex), buffer)) { + throw new MediaPipeException( + MediaPipeException.StatusCode.INTERNAL.ordinal(), + "There is an error getting category mask."); + } + } else { + buffer = PacketGetter.getImageDataDirectly(packets.get(categoryMaskOutStreamIndex)); + } ByteBufferImageBuilder builder = - new ByteBufferImageBuilder(buffer, width, height, imageFormat); - segmentedMasks.add(builder.build()); + new ByteBufferImageBuilder(buffer, width, height, MPImage.IMAGE_FORMAT_ALPHA); + categoryMask = Optional.of(builder.build()); } return ImageSegmenterResult.create( - Optional.of(segmentedMasks), - Optional.empty(), + confidenceMasks, + categoryMask, BaseVisionTaskApi.generateResultTimestampMs( - RunningMode.IMAGE, packets.get(GROUPED_SEGMENTATION_OUT_STREAM_INDEX))); + RunningMode.IMAGE, packets.get(IMAGE_OUT_STREAM_INDEX))); } @Override @@ -195,7 +222,7 @@ public final class InteractiveSegmenter extends BaseVisionTaskApi { .setTaskRunningModeName(RunningMode.IMAGE.name()) .setTaskGraphName(TASK_GRAPH_NAME) .setInputStreams(INPUT_STREAMS) - .setOutputStreams(OUTPUT_STREAMS) + .setOutputStreams(outputStreams) .setTaskOptions(segmenterOptions) .setEnableFlowLimiting(false) .build(), @@ -394,8 +421,11 @@ public final class InteractiveSegmenter extends BaseVisionTaskApi { /** Sets the base options for the image segmenter task. */ public abstract Builder setBaseOptions(BaseOptions value); - /** The output type from image segmenter. */ - public abstract Builder setOutputType(OutputType value); + /** Sets whether to output confidence masks. Default to true. */ + public abstract Builder setOutputConfidenceMasks(boolean value); + + /** Sets whether to output category mask. Default to false. */ + public abstract Builder setOutputCategoryMask(boolean value); /** * Sets an optional {@link ResultListener} to receive the segmentation results when the graph @@ -417,25 +447,18 @@ public final class InteractiveSegmenter extends BaseVisionTaskApi { abstract BaseOptions baseOptions(); - abstract OutputType outputType(); + abstract boolean outputConfidenceMasks(); + + abstract boolean outputCategoryMask(); abstract Optional> resultListener(); abstract Optional errorListener(); - /** The output type of segmentation results. */ - public enum OutputType { - // Gives a single output mask where each pixel represents the class which - // the pixel in the original image was predicted to belong to. - CATEGORY_MASK, - // Gives a list of output masks where, for each mask, each pixel represents - // the prediction confidence, usually in the [0, 1] range. - CONFIDENCE_MASK - } - public static Builder builder() { return new AutoValue_InteractiveSegmenter_InteractiveSegmenterOptions.Builder() - .setOutputType(OutputType.CATEGORY_MASK); + .setOutputConfidenceMasks(true) + .setOutputCategoryMask(false); } /** @@ -454,14 +477,6 @@ public final class InteractiveSegmenter extends BaseVisionTaskApi { SegmenterOptionsProto.SegmenterOptions.Builder segmenterOptionsBuilder = SegmenterOptionsProto.SegmenterOptions.newBuilder(); - if (outputType() == OutputType.CONFIDENCE_MASK) { - segmenterOptionsBuilder.setOutputType( - SegmenterOptionsProto.SegmenterOptions.OutputType.CONFIDENCE_MASK); - } else if (outputType() == OutputType.CATEGORY_MASK) { - segmenterOptionsBuilder.setOutputType( - SegmenterOptionsProto.SegmenterOptions.OutputType.CATEGORY_MASK); - } - taskOptionsBuilder.setSegmenterOptions(segmenterOptionsBuilder); return CalculatorOptions.newBuilder() .setExtension( diff --git a/mediapipe/tasks/javatests/com/google/mediapipe/tasks/vision/interactivesegmenter/InteractiveSegmenterTest.java b/mediapipe/tasks/javatests/com/google/mediapipe/tasks/vision/interactivesegmenter/InteractiveSegmenterTest.java index f32ab797..3a685494 100644 --- a/mediapipe/tasks/javatests/com/google/mediapipe/tasks/vision/interactivesegmenter/InteractiveSegmenterTest.java +++ b/mediapipe/tasks/javatests/com/google/mediapipe/tasks/vision/interactivesegmenter/InteractiveSegmenterTest.java @@ -53,18 +53,15 @@ public class InteractiveSegmenterTest { InteractiveSegmenterOptions options = InteractiveSegmenterOptions.builder() .setBaseOptions(BaseOptions.builder().setModelAssetPath(DEEPLAB_MODEL_FILE).build()) - .setOutputType(InteractiveSegmenterOptions.OutputType.CATEGORY_MASK) + .setOutputConfidenceMasks(false) + .setOutputCategoryMask(true) .build(); InteractiveSegmenter imageSegmenter = InteractiveSegmenter.createFromOptions( ApplicationProvider.getApplicationContext(), options); MPImage image = getImageFromAsset(inputImageName); ImageSegmenterResult actualResult = imageSegmenter.segment(image, roi); - // TODO update to correct category mask output. - // After InteractiveSegmenter updated according to (b/276519300), update this to use - // categoryMask field instead of confidenceMasks. - List segmentations = actualResult.confidenceMasks().get(); - assertThat(segmentations.size()).isEqualTo(1); + assertThat(actualResult.categoryMask().isPresent()).isTrue(); } @Test @@ -75,15 +72,17 @@ public class InteractiveSegmenterTest { InteractiveSegmenterOptions options = InteractiveSegmenterOptions.builder() .setBaseOptions(BaseOptions.builder().setModelAssetPath(DEEPLAB_MODEL_FILE).build()) - .setOutputType(InteractiveSegmenterOptions.OutputType.CONFIDENCE_MASK) + .setOutputConfidenceMasks(true) + .setOutputCategoryMask(false) .build(); InteractiveSegmenter imageSegmenter = InteractiveSegmenter.createFromOptions( ApplicationProvider.getApplicationContext(), options); ImageSegmenterResult actualResult = imageSegmenter.segment(getImageFromAsset(inputImageName), roi); - List segmentations = actualResult.confidenceMasks().get(); - assertThat(segmentations.size()).isEqualTo(2); + assertThat(actualResult.confidenceMasks().isPresent()).isTrue(); + List confidenceMasks = actualResult.confidenceMasks().get(); + assertThat(confidenceMasks.size()).isEqualTo(2); } } From 5fc0d26d8d382cc13ae6852e52e8a28297abb6fd Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Fri, 14 Apr 2023 21:31:09 -0700 Subject: [PATCH 16/30] Internal change PiperOrigin-RevId: 524457848 --- .../core/constant_side_packet_calculator.cc | 4 +- .../calculators/core/gate_calculator_test.cc | 68 +++++++++---------- .../core/string_to_int_calculator.cc | 8 +-- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/mediapipe/calculators/core/constant_side_packet_calculator.cc b/mediapipe/calculators/core/constant_side_packet_calculator.cc index 45ff0711..509f7e9d 100644 --- a/mediapipe/calculators/core/constant_side_packet_calculator.cc +++ b/mediapipe/calculators/core/constant_side_packet_calculator.cc @@ -78,7 +78,7 @@ class ConstantSidePacketCalculator : public CalculatorBase { } else if (packet_options.has_string_value()) { packet.Set(); } else if (packet_options.has_uint64_value()) { - packet.Set(); + packet.Set(); } else if (packet_options.has_classification_list_value()) { packet.Set(); } else if (packet_options.has_landmark_list_value()) { @@ -112,7 +112,7 @@ class ConstantSidePacketCalculator : public CalculatorBase { } else if (packet_options.has_string_value()) { packet.Set(MakePacket(packet_options.string_value())); } else if (packet_options.has_uint64_value()) { - packet.Set(MakePacket(packet_options.uint64_value())); + packet.Set(MakePacket(packet_options.uint64_value())); } else if (packet_options.has_classification_list_value()) { packet.Set(MakePacket( packet_options.classification_list_value())); diff --git a/mediapipe/calculators/core/gate_calculator_test.cc b/mediapipe/calculators/core/gate_calculator_test.cc index c734ddb5..19201982 100644 --- a/mediapipe/calculators/core/gate_calculator_test.cc +++ b/mediapipe/calculators/core/gate_calculator_test.cc @@ -35,14 +35,14 @@ class GateCalculatorTest : public ::testing::Test { } // Use this when ALLOW/DISALLOW input is provided as a side packet. - void RunTimeStep(int64 timestamp, bool stream_payload) { + void RunTimeStep(int64_t timestamp, bool stream_payload) { runner_->MutableInputs()->Get("", 0).packets.push_back( MakePacket(stream_payload).At(Timestamp(timestamp))); MP_ASSERT_OK(runner_->Run()) << "Calculator execution failed."; } // Use this when ALLOW/DISALLOW input is provided as an input stream. - void RunTimeStep(int64 timestamp, const std::string& control_tag, + void RunTimeStep(int64_t timestamp, const std::string& control_tag, bool control) { runner_->MutableInputs()->Get("", 0).packets.push_back( MakePacket(true).At(Timestamp(timestamp))); @@ -134,9 +134,9 @@ TEST_F(GateCalculatorTest, AllowByALLOWOptionToTrue) { } )"); - constexpr int64 kTimestampValue0 = 42; + constexpr int64_t kTimestampValue0 = 42; RunTimeStep(kTimestampValue0, true); - constexpr int64 kTimestampValue1 = 43; + constexpr int64_t kTimestampValue1 = 43; RunTimeStep(kTimestampValue1, false); const std::vector& output = runner()->Outputs().Get("", 0).packets; @@ -159,9 +159,9 @@ TEST_F(GateCalculatorTest, DisallowByALLOWOptionSetToFalse) { } )"); - constexpr int64 kTimestampValue0 = 42; + constexpr int64_t kTimestampValue0 = 42; RunTimeStep(kTimestampValue0, true); - constexpr int64 kTimestampValue1 = 43; + constexpr int64_t kTimestampValue1 = 43; RunTimeStep(kTimestampValue1, false); const std::vector& output = runner()->Outputs().Get("", 0).packets; @@ -175,9 +175,9 @@ TEST_F(GateCalculatorTest, DisallowByALLOWOptionNotSet) { output_stream: "test_output" )"); - constexpr int64 kTimestampValue0 = 42; + constexpr int64_t kTimestampValue0 = 42; RunTimeStep(kTimestampValue0, true); - constexpr int64 kTimestampValue1 = 43; + constexpr int64_t kTimestampValue1 = 43; RunTimeStep(kTimestampValue1, false); const std::vector& output = runner()->Outputs().Get("", 0).packets; @@ -193,9 +193,9 @@ TEST_F(GateCalculatorTest, AllowByALLOWSidePacketSetToTrue) { )"); runner()->MutableSidePackets()->Tag(kAllowTag) = Adopt(new bool(true)); - constexpr int64 kTimestampValue0 = 42; + constexpr int64_t kTimestampValue0 = 42; RunTimeStep(kTimestampValue0, true); - constexpr int64 kTimestampValue1 = 43; + constexpr int64_t kTimestampValue1 = 43; RunTimeStep(kTimestampValue1, false); const std::vector& output = runner()->Outputs().Get("", 0).packets; @@ -215,9 +215,9 @@ TEST_F(GateCalculatorTest, AllowByDisallowSidePacketSetToFalse) { )"); runner()->MutableSidePackets()->Tag(kDisallowTag) = Adopt(new bool(false)); - constexpr int64 kTimestampValue0 = 42; + constexpr int64_t kTimestampValue0 = 42; RunTimeStep(kTimestampValue0, true); - constexpr int64 kTimestampValue1 = 43; + constexpr int64_t kTimestampValue1 = 43; RunTimeStep(kTimestampValue1, false); const std::vector& output = runner()->Outputs().Get("", 0).packets; @@ -237,9 +237,9 @@ TEST_F(GateCalculatorTest, DisallowByALLOWSidePacketSetToFalse) { )"); runner()->MutableSidePackets()->Tag(kAllowTag) = Adopt(new bool(false)); - constexpr int64 kTimestampValue0 = 42; + constexpr int64_t kTimestampValue0 = 42; RunTimeStep(kTimestampValue0, true); - constexpr int64 kTimestampValue1 = 43; + constexpr int64_t kTimestampValue1 = 43; RunTimeStep(kTimestampValue1, false); const std::vector& output = runner()->Outputs().Get("", 0).packets; @@ -255,9 +255,9 @@ TEST_F(GateCalculatorTest, DisallowByDISALLOWSidePacketSetToTrue) { )"); runner()->MutableSidePackets()->Tag(kDisallowTag) = Adopt(new bool(true)); - constexpr int64 kTimestampValue0 = 42; + constexpr int64_t kTimestampValue0 = 42; RunTimeStep(kTimestampValue0, true); - constexpr int64 kTimestampValue1 = 43; + constexpr int64_t kTimestampValue1 = 43; RunTimeStep(kTimestampValue1, false); const std::vector& output = runner()->Outputs().Get("", 0).packets; @@ -272,13 +272,13 @@ TEST_F(GateCalculatorTest, Allow) { output_stream: "test_output" )"); - constexpr int64 kTimestampValue0 = 42; + constexpr int64_t kTimestampValue0 = 42; RunTimeStep(kTimestampValue0, "ALLOW", true); - constexpr int64 kTimestampValue1 = 43; + constexpr int64_t kTimestampValue1 = 43; RunTimeStep(kTimestampValue1, "ALLOW", false); - constexpr int64 kTimestampValue2 = 44; + constexpr int64_t kTimestampValue2 = 44; RunTimeStep(kTimestampValue2, "ALLOW", true); - constexpr int64 kTimestampValue3 = 45; + constexpr int64_t kTimestampValue3 = 45; RunTimeStep(kTimestampValue3, "ALLOW", false); const std::vector& output = runner()->Outputs().Get("", 0).packets; @@ -297,13 +297,13 @@ TEST_F(GateCalculatorTest, Disallow) { output_stream: "test_output" )"); - constexpr int64 kTimestampValue0 = 42; + constexpr int64_t kTimestampValue0 = 42; RunTimeStep(kTimestampValue0, "DISALLOW", true); - constexpr int64 kTimestampValue1 = 43; + constexpr int64_t kTimestampValue1 = 43; RunTimeStep(kTimestampValue1, "DISALLOW", false); - constexpr int64 kTimestampValue2 = 44; + constexpr int64_t kTimestampValue2 = 44; RunTimeStep(kTimestampValue2, "DISALLOW", true); - constexpr int64 kTimestampValue3 = 45; + constexpr int64_t kTimestampValue3 = 45; RunTimeStep(kTimestampValue3, "DISALLOW", false); const std::vector& output = runner()->Outputs().Get("", 0).packets; @@ -323,13 +323,13 @@ TEST_F(GateCalculatorTest, AllowWithStateChange) { output_stream: "STATE_CHANGE:state_changed" )"); - constexpr int64 kTimestampValue0 = 42; + constexpr int64_t kTimestampValue0 = 42; RunTimeStep(kTimestampValue0, "ALLOW", false); - constexpr int64 kTimestampValue1 = 43; + constexpr int64_t kTimestampValue1 = 43; RunTimeStep(kTimestampValue1, "ALLOW", true); - constexpr int64 kTimestampValue2 = 44; + constexpr int64_t kTimestampValue2 = 44; RunTimeStep(kTimestampValue2, "ALLOW", true); - constexpr int64 kTimestampValue3 = 45; + constexpr int64_t kTimestampValue3 = 45; RunTimeStep(kTimestampValue3, "ALLOW", false); const std::vector& output = @@ -379,13 +379,13 @@ TEST_F(GateCalculatorTest, DisallowWithStateChange) { output_stream: "STATE_CHANGE:state_changed" )"); - constexpr int64 kTimestampValue0 = 42; + constexpr int64_t kTimestampValue0 = 42; RunTimeStep(kTimestampValue0, "DISALLOW", true); - constexpr int64 kTimestampValue1 = 43; + constexpr int64_t kTimestampValue1 = 43; RunTimeStep(kTimestampValue1, "DISALLOW", false); - constexpr int64 kTimestampValue2 = 44; + constexpr int64_t kTimestampValue2 = 44; RunTimeStep(kTimestampValue2, "DISALLOW", false); - constexpr int64 kTimestampValue3 = 45; + constexpr int64_t kTimestampValue3 = 45; RunTimeStep(kTimestampValue3, "DISALLOW", true); const std::vector& output = @@ -432,7 +432,7 @@ TEST_F(GateCalculatorTest, DisallowInitialNoStateTransition) { output_stream: "STATE_CHANGE:state_changed" )"); - constexpr int64 kTimestampValue0 = 42; + constexpr int64_t kTimestampValue0 = 42; RunTimeStep(kTimestampValue0, "DISALLOW", false); const std::vector& output = @@ -450,7 +450,7 @@ TEST_F(GateCalculatorTest, AllowInitialNoStateTransition) { output_stream: "STATE_CHANGE:state_changed" )"); - constexpr int64 kTimestampValue0 = 42; + constexpr int64_t kTimestampValue0 = 42; RunTimeStep(kTimestampValue0, "ALLOW", true); const std::vector& output = diff --git a/mediapipe/calculators/core/string_to_int_calculator.cc b/mediapipe/calculators/core/string_to_int_calculator.cc index ecd55afb..fa67aa8e 100644 --- a/mediapipe/calculators/core/string_to_int_calculator.cc +++ b/mediapipe/calculators/core/string_to_int_calculator.cc @@ -64,16 +64,16 @@ REGISTER_CALCULATOR(StringToIntCalculator); using StringToUintCalculator = StringToIntCalculatorTemplate; REGISTER_CALCULATOR(StringToUintCalculator); -using StringToInt32Calculator = StringToIntCalculatorTemplate; +using StringToInt32Calculator = StringToIntCalculatorTemplate; REGISTER_CALCULATOR(StringToInt32Calculator); -using StringToUint32Calculator = StringToIntCalculatorTemplate; +using StringToUint32Calculator = StringToIntCalculatorTemplate; REGISTER_CALCULATOR(StringToUint32Calculator); -using StringToInt64Calculator = StringToIntCalculatorTemplate; +using StringToInt64Calculator = StringToIntCalculatorTemplate; REGISTER_CALCULATOR(StringToInt64Calculator); -using StringToUint64Calculator = StringToIntCalculatorTemplate; +using StringToUint64Calculator = StringToIntCalculatorTemplate; REGISTER_CALCULATOR(StringToUint64Calculator); } // namespace mediapipe From 1c3a061038382f0e3d0c73dd370e3c42807cc8c5 Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Sat, 15 Apr 2023 00:22:57 -0700 Subject: [PATCH 17/30] Internal change PiperOrigin-RevId: 524481964 --- mediapipe/gpu/gpu_buffer_storage_yuv_image.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mediapipe/gpu/gpu_buffer_storage_yuv_image.cc b/mediapipe/gpu/gpu_buffer_storage_yuv_image.cc index c7acd134..4b0913b9 100644 --- a/mediapipe/gpu/gpu_buffer_storage_yuv_image.cc +++ b/mediapipe/gpu/gpu_buffer_storage_yuv_image.cc @@ -167,7 +167,7 @@ GpuBufferStorageYuvImage::GpuBufferStorageYuvImage(int width, int height, GpuBufferFormat format) { libyuv::FourCC fourcc = FourCCForGpuBufferFormat(format); int y_stride = std::ceil(1.0f * width / kDefaultDataAligment); - auto y_data = std::make_unique(y_stride * height); + auto y_data = std::make_unique(y_stride * height); switch (fourcc) { case libyuv::FOURCC_NV12: case libyuv::FOURCC_NV21: { @@ -175,7 +175,7 @@ GpuBufferStorageYuvImage::GpuBufferStorageYuvImage(int width, int height, int uv_width = 2 * std::ceil(0.5f * width); int uv_height = std::ceil(0.5f * height); int uv_stride = std::ceil(1.0f * uv_width / kDefaultDataAligment); - auto uv_data = std::make_unique(uv_stride * uv_height); + auto uv_data = std::make_unique(uv_stride * uv_height); yuv_image_ = std::make_shared( fourcc, std::move(y_data), y_stride, std::move(uv_data), uv_stride, nullptr, 0, width, height); @@ -187,8 +187,8 @@ GpuBufferStorageYuvImage::GpuBufferStorageYuvImage(int width, int height, int uv_width = std::ceil(0.5f * width); int uv_height = std::ceil(0.5f * height); int uv_stride = std::ceil(1.0f * uv_width / kDefaultDataAligment); - auto u_data = std::make_unique(uv_stride * uv_height); - auto v_data = std::make_unique(uv_stride * uv_height); + auto u_data = std::make_unique(uv_stride * uv_height); + auto v_data = std::make_unique(uv_stride * uv_height); yuv_image_ = std::make_shared( fourcc, std::move(y_data), y_stride, std::move(u_data), uv_stride, std::move(v_data), uv_stride, width, height); From b63a0e15a3a769a862b1ffec139264727a21a00e Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Sat, 15 Apr 2023 00:57:45 -0700 Subject: [PATCH 18/30] Internal change PiperOrigin-RevId: 524485761 --- mediapipe/util/cpu_util.cc | 14 +++---- mediapipe/util/image_frame_util.cc | 59 +++++++++++++++--------------- mediapipe/util/label_map_util.cc | 4 +- 3 files changed, 39 insertions(+), 38 deletions(-) diff --git a/mediapipe/util/cpu_util.cc b/mediapipe/util/cpu_util.cc index c1be9793..052eabb8 100644 --- a/mediapipe/util/cpu_util.cc +++ b/mediapipe/util/cpu_util.cc @@ -43,7 +43,7 @@ ABSL_FLAG(std::string, system_cpu_max_freq_file, namespace mediapipe { namespace { -constexpr uint32 kBufferLength = 64; +constexpr uint32_t kBufferLength = 64; absl::StatusOr GetFilePath(int cpu) { if (!absl::StrContains(absl::GetFlag(FLAGS_system_cpu_max_freq_file), "$0")) { @@ -54,7 +54,7 @@ absl::StatusOr GetFilePath(int cpu) { return absl::Substitute(absl::GetFlag(FLAGS_system_cpu_max_freq_file), cpu); } -absl::StatusOr GetCpuMaxFrequency(int cpu) { +absl::StatusOr GetCpuMaxFrequency(int cpu) { auto path_or_status = GetFilePath(cpu); if (!path_or_status.ok()) { return path_or_status.status(); @@ -65,7 +65,7 @@ absl::StatusOr GetCpuMaxFrequency(int cpu) { char buffer[kBufferLength]; file.getline(buffer, kBufferLength); file.close(); - uint64 frequency; + uint64_t frequency; if (absl::SimpleAtoi(buffer, &frequency)) { return frequency; } else { @@ -79,7 +79,7 @@ absl::StatusOr GetCpuMaxFrequency(int cpu) { } std::set InferLowerOrHigherCoreIds(bool lower) { - std::vector> cpu_freq_pairs; + std::vector> cpu_freq_pairs; for (int cpu = 0; cpu < NumCPUCores(); ++cpu) { auto freq_or_status = GetCpuMaxFrequency(cpu); if (freq_or_status.ok()) { @@ -90,12 +90,12 @@ std::set InferLowerOrHigherCoreIds(bool lower) { return {}; } - absl::c_sort(cpu_freq_pairs, [lower](const std::pair& left, - const std::pair& right) { + absl::c_sort(cpu_freq_pairs, [lower](const std::pair& left, + const std::pair& right) { return (lower && left.second < right.second) || (!lower && left.second > right.second); }); - uint64 edge_freq = cpu_freq_pairs[0].second; + uint64_t edge_freq = cpu_freq_pairs[0].second; std::set inferred_cores; for (const auto& cpu_freq_pair : cpu_freq_pairs) { diff --git a/mediapipe/util/image_frame_util.cc b/mediapipe/util/image_frame_util.cc index a3a038b0..bf2773fd 100644 --- a/mediapipe/util/image_frame_util.cc +++ b/mediapipe/util/image_frame_util.cc @@ -89,12 +89,12 @@ void ImageFrameToYUVImage(const ImageFrame& image_frame, YUVImage* yuv_image) { const int uv_stride = (uv_width + 15) & ~15; const int y_size = y_stride * height; const int uv_size = uv_stride * uv_height; - uint8* data = - reinterpret_cast(aligned_malloc(y_size + uv_size * 2, 16)); + uint8_t* data = + reinterpret_cast(aligned_malloc(y_size + uv_size * 2, 16)); std::function deallocate = [data]() { aligned_free(data); }; - uint8* y = data; - uint8* u = y + y_size; - uint8* v = u + uv_size; + uint8_t* y = data; + uint8_t* u = y + y_size; + uint8_t* v = u + uv_size; yuv_image->Initialize(libyuv::FOURCC_I420, deallocate, // y, y_stride, // u, uv_stride, // @@ -123,10 +123,11 @@ void ImageFrameToYUVNV12Image(const ImageFrame& image_frame, const int uv_stride = y_stride; const int uv_height = (height + 1) / 2; const int uv_size = uv_stride * uv_height; - uint8* data = reinterpret_cast(aligned_malloc(y_size + uv_size, 16)); + uint8_t* data = + reinterpret_cast(aligned_malloc(y_size + uv_size, 16)); std::function deallocate = [data] { aligned_free(data); }; - uint8* y = data; - uint8* uv = y + y_size; + uint8_t* y = data; + uint8_t* uv = y + y_size; yuv_nv12_image->Initialize(libyuv::FOURCC_NV12, deallocate, y, y_stride, uv, uv_stride, nullptr, 0, width, height); const int rv = libyuv::I420ToNV12( @@ -210,44 +211,44 @@ void YUVImageToImageFrameFromFormat(const YUVImage& yuv_image, } } -void SrgbToMpegYCbCr(const uint8 r, const uint8 g, const uint8 b, // - uint8* y, uint8* cb, uint8* cr) { +void SrgbToMpegYCbCr(const uint8_t r, const uint8_t g, const uint8_t b, // + uint8_t* y, uint8_t* cb, uint8_t* cr) { // ITU-R BT.601 conversion from sRGB to YCbCr. // FastIntRound is used rather than SafeRound since the possible // range of values is [16,235] for Y and [16,240] for Cb and Cr and we // don't care about the rounding direction for values exactly between // two integers. - *y = static_cast( + *y = static_cast( mediapipe::MathUtil::FastIntRound(16.0 + // 65.481 * r / 255.0 + // 128.553 * g / 255.0 + // 24.966 * b / 255.0)); - *cb = static_cast( + *cb = static_cast( mediapipe::MathUtil::FastIntRound(128.0 + // -37.797 * r / 255.0 + // -74.203 * g / 255.0 + // 112.0 * b / 255.0)); - *cr = static_cast( + *cr = static_cast( mediapipe::MathUtil::FastIntRound(128.0 + // 112.0 * r / 255.0 + // -93.786 * g / 255.0 + // -18.214 * b / 255.0)); } -void MpegYCbCrToSrgb(const uint8 y, const uint8 cb, const uint8 cr, // - uint8* r, uint8* g, uint8* b) { +void MpegYCbCrToSrgb(const uint8_t y, const uint8_t cb, const uint8_t cr, // + uint8_t* r, uint8_t* g, uint8_t* b) { // ITU-R BT.601 conversion from YCbCr to sRGB // Use SafeRound since many MPEG YCbCr values do not correspond directly // to an sRGB value. - *r = mediapipe::MathUtil::SafeRound( // - 255.0 / 219.0 * (y - 16.0) + // + *r = mediapipe::MathUtil::SafeRound( // + 255.0 / 219.0 * (y - 16.0) + // 255.0 / 112.0 * 0.701 * (cr - 128.0)); - *g = mediapipe::MathUtil::SafeRound( + *g = mediapipe::MathUtil::SafeRound( 255.0 / 219.0 * (y - 16.0) - // 255.0 / 112.0 * 0.886 * 0.114 / 0.587 * (cb - 128.0) - // 255.0 / 112.0 * 0.701 * 0.299 / 0.587 * (cr - 128.0)); - *b = mediapipe::MathUtil::SafeRound( // - 255.0 / 219.0 * (y - 16.0) + // + *b = mediapipe::MathUtil::SafeRound( // + 255.0 / 219.0 * (y - 16.0) + // 255.0 / 112.0 * 0.886 * (cb - 128.0)); } @@ -260,15 +261,15 @@ void MpegYCbCrToSrgb(const uint8 y, const uint8 cb, const uint8 cr, // cv::Mat GetSrgbToLinearRgb16Lut() { cv::Mat lut(1, 256, CV_16UC1); - uint16* ptr = lut.ptr(); + uint16_t* ptr = lut.ptr(); constexpr double kUint8Max = 255.0; constexpr double kUint16Max = 65535.0; for (int i = 0; i < 256; ++i) { if (i < 0.04045 * kUint8Max) { - ptr[i] = static_cast( + ptr[i] = static_cast( (static_cast(i) / kUint8Max / 12.92) * kUint16Max + .5); } else { - ptr[i] = static_cast( + ptr[i] = static_cast( pow((static_cast(i) / kUint8Max + 0.055) / 1.055, 2.4) * kUint16Max + .5); @@ -279,15 +280,15 @@ cv::Mat GetSrgbToLinearRgb16Lut() { cv::Mat GetLinearRgb16ToSrgbLut() { cv::Mat lut(1, 65536, CV_8UC1); - uint8* ptr = lut.ptr(); + uint8_t* ptr = lut.ptr(); constexpr double kUint8Max = 255.0; constexpr double kUint16Max = 65535.0; for (int i = 0; i < 65536; ++i) { if (i < 0.0031308 * kUint16Max) { - ptr[i] = static_cast( + ptr[i] = static_cast( (static_cast(i) / kUint16Max * 12.92) * kUint8Max + .5); } else { - ptr[i] = static_cast( + ptr[i] = static_cast( (1.055 * pow(static_cast(i) / kUint16Max, 1.0 / 2.4) - .055) * kUint8Max + .5); @@ -306,13 +307,13 @@ void LinearRgb16ToSrgb(const cv::Mat& source, cv::Mat* destination) { destination->create(source.size(), CV_8UC(source.channels())); static const cv::Mat kLut = GetLinearRgb16ToSrgbLut(); - const uint8* lookup_table_ptr = kLut.ptr(); + const uint8_t* lookup_table_ptr = kLut.ptr(); const int num_channels = source.channels(); for (int row = 0; row < source.rows; ++row) { for (int col = 0; col < source.cols; ++col) { for (int channel = 0; channel < num_channels; ++channel) { - uint8* ptr = destination->ptr(row); - const uint16* ptr16 = source.ptr(row); + uint8_t* ptr = destination->ptr(row); + const uint16_t* ptr16 = source.ptr(row); ptr[col * num_channels + channel] = lookup_table_ptr[ptr16[col * num_channels + channel]]; } diff --git a/mediapipe/util/label_map_util.cc b/mediapipe/util/label_map_util.cc index 914a2ba7..eb909349 100644 --- a/mediapipe/util/label_map_util.cc +++ b/mediapipe/util/label_map_util.cc @@ -25,7 +25,7 @@ namespace mediapipe { -absl::StatusOr> BuildLabelMapFromFiles( +absl::StatusOr> BuildLabelMapFromFiles( absl::string_view labels_file_contents, absl::string_view display_names_file) { if (labels_file_contents.empty()) { @@ -68,7 +68,7 @@ absl::StatusOr> BuildLabelMapFromFiles( label_map_items[i].set_display_name(display_names[i]); } } - proto_ns::Map label_map; + proto_ns::Map label_map; for (int i = 0; i < label_map_items.size(); ++i) { label_map[i] = label_map_items[i]; } From 547d3c00a72e7b87675b6c0d9f267d5b5bad67db Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Sat, 15 Apr 2023 02:32:12 -0700 Subject: [PATCH 19/30] Internal change PiperOrigin-RevId: 524496413 --- .../stream_handler/fixed_size_input_stream_handler.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mediapipe/framework/stream_handler/fixed_size_input_stream_handler.cc b/mediapipe/framework/stream_handler/fixed_size_input_stream_handler.cc index d53acedc..fd51a738 100644 --- a/mediapipe/framework/stream_handler/fixed_size_input_stream_handler.cc +++ b/mediapipe/framework/stream_handler/fixed_size_input_stream_handler.cc @@ -131,9 +131,9 @@ class FixedSizeInputStreamHandler : public DefaultInputStreamHandler { ABSL_EXCLUSIVE_LOCKS_REQUIRED(erase_mutex_) { // Record the most recent first kept timestamp on any stream. for (const auto& stream : input_stream_managers_) { - int32 queue_size = (stream->QueueSize() >= trigger_queue_size_) - ? target_queue_size_ - : trigger_queue_size_ - 1; + int32_t queue_size = (stream->QueueSize() >= trigger_queue_size_) + ? target_queue_size_ + : trigger_queue_size_ - 1; if (stream->QueueSize() > queue_size) { kept_timestamp_ = std::max( kept_timestamp_, stream->GetMinTimestampAmongNLatest(queue_size + 1) @@ -214,8 +214,8 @@ class FixedSizeInputStreamHandler : public DefaultInputStreamHandler { } private: - int32 trigger_queue_size_; - int32 target_queue_size_; + int32_t trigger_queue_size_; + int32_t target_queue_size_; bool fixed_min_size_; // Indicates that GetNodeReadiness has returned kReadyForProcess once, and // the corresponding call to FillInputSet has not yet completed. From 48cc96cf3c9d7e1eee20c36f90e7e2dcc497a6fb Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Sat, 15 Apr 2023 06:16:04 -0700 Subject: [PATCH 20/30] Internal change PiperOrigin-RevId: 524517985 --- mediapipe/calculators/image/warp_affine_calculator.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mediapipe/calculators/image/warp_affine_calculator.cc b/mediapipe/calculators/image/warp_affine_calculator.cc index 38870177..dcc37103 100644 --- a/mediapipe/calculators/image/warp_affine_calculator.cc +++ b/mediapipe/calculators/image/warp_affine_calculator.cc @@ -166,7 +166,7 @@ class WarpAffineRunnerHolder { const ImageFrame image_frame(frame_ptr->Format(), frame_ptr->Width(), frame_ptr->Height(), frame_ptr->WidthStep(), const_cast(frame_ptr->PixelData()), - [](uint8* data){}); + [](uint8_t* data){}); ASSIGN_OR_RETURN(auto result, runner->Run(image_frame, matrix, size, border_mode)); return mediapipe::Image(std::make_shared(std::move(result))); From b147002b7eacd624b1d8e2b133604f4703577c50 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Mon, 17 Apr 2023 13:51:59 -0700 Subject: [PATCH 21/30] Support new output format for InteractiveSegmenter PiperOrigin-RevId: 524940992 --- mediapipe/tasks/web/vision/core/types.d.ts | 11 -- .../web/vision/interactive_segmenter/BUILD | 5 +- .../interactive_segmenter.ts | 109 +++++++++---- .../interactive_segmenter_options.d.ts | 19 +-- .../interactive_segmenter_result.d.ts | 37 +++++ .../interactive_segmenter_test.ts | 143 +++++++++++------- 6 files changed, 211 insertions(+), 113 deletions(-) create mode 100644 mediapipe/tasks/web/vision/interactive_segmenter/interactive_segmenter_result.d.ts diff --git a/mediapipe/tasks/web/vision/core/types.d.ts b/mediapipe/tasks/web/vision/core/types.d.ts index 5699126b..344d4db8 100644 --- a/mediapipe/tasks/web/vision/core/types.d.ts +++ b/mediapipe/tasks/web/vision/core/types.d.ts @@ -25,17 +25,6 @@ import {NormalizedKeypoint} from '../../../../tasks/web/components/containers/ke */ export type SegmentationMask = Uint8ClampedArray|Float32Array|WebGLTexture; -/** - * A callback that receives the computed masks from the segmentation tasks. The - * callback either receives a single element array with a category mask (as a - * `[Uint8ClampedArray]`) or multiple confidence masks (as a `Float32Array[]`). - * The returned data is only valid for the duration of the callback. If - * asynchronous processing is needed, all data needs to be copied before the - * callback returns. - */ -export type SegmentationMaskCallback = - (masks: SegmentationMask[], width: number, height: number) => void; - /** * A callback that receives an `ImageData` object from a Vision task. The * lifetime of the underlying data is limited to the duration of the callback. diff --git a/mediapipe/tasks/web/vision/interactive_segmenter/BUILD b/mediapipe/tasks/web/vision/interactive_segmenter/BUILD index a4a3f27c..ead85d38 100644 --- a/mediapipe/tasks/web/vision/interactive_segmenter/BUILD +++ b/mediapipe/tasks/web/vision/interactive_segmenter/BUILD @@ -30,7 +30,10 @@ mediapipe_ts_library( mediapipe_ts_declaration( name = "interactive_segmenter_types", - srcs = ["interactive_segmenter_options.d.ts"], + srcs = [ + "interactive_segmenter_options.d.ts", + "interactive_segmenter_result.d.ts", + ], deps = [ "//mediapipe/tasks/web/core", "//mediapipe/tasks/web/core:classifier_options", diff --git a/mediapipe/tasks/web/vision/interactive_segmenter/interactive_segmenter.ts b/mediapipe/tasks/web/vision/interactive_segmenter/interactive_segmenter.ts index ddcc7e59..16841bd7 100644 --- a/mediapipe/tasks/web/vision/interactive_segmenter/interactive_segmenter.ts +++ b/mediapipe/tasks/web/vision/interactive_segmenter/interactive_segmenter.ts @@ -21,7 +21,7 @@ import {ImageSegmenterGraphOptions as ImageSegmenterGraphOptionsProto} from '../ import {SegmenterOptions as SegmenterOptionsProto} from '../../../../tasks/cc/vision/image_segmenter/proto/segmenter_options_pb'; import {WasmFileset} from '../../../../tasks/web/core/wasm_fileset'; import {ImageProcessingOptions} from '../../../../tasks/web/vision/core/image_processing_options'; -import {RegionOfInterest, SegmentationMask, SegmentationMaskCallback} from '../../../../tasks/web/vision/core/types'; +import {RegionOfInterest, SegmentationMask} from '../../../../tasks/web/vision/core/types'; import {VisionGraphRunner, VisionTaskRunner} from '../../../../tasks/web/vision/core/vision_task_runner'; import {Color as ColorProto} from '../../../../util/color_pb'; import {RenderAnnotation as RenderAnnotationProto, RenderData as RenderDataProto} from '../../../../util/render_data_pb'; @@ -29,21 +29,35 @@ import {ImageSource, WasmModule} from '../../../../web/graph_runner/graph_runner // Placeholder for internal dependency on trusted resource url import {InteractiveSegmenterOptions} from './interactive_segmenter_options'; +import {InteractiveSegmenterResult} from './interactive_segmenter_result'; export * from './interactive_segmenter_options'; -export {SegmentationMask, SegmentationMaskCallback, RegionOfInterest}; +export * from './interactive_segmenter_result'; +export {SegmentationMask, RegionOfInterest}; export {ImageSource}; const IMAGE_IN_STREAM = 'image_in'; const NORM_RECT_IN_STREAM = 'norm_rect_in'; const ROI_IN_STREAM = 'roi_in'; -const IMAGE_OUT_STREAM = 'image_out'; +const CONFIDENCE_MASKS_STREAM = 'confidence_masks'; +const CATEGORY_MASK_STREAM = 'category_mask'; const IMAGEA_SEGMENTER_GRAPH = 'mediapipe.tasks.vision.interactive_segmenter.InteractiveSegmenterGraph'; +const DEFAULT_OUTPUT_CATEGORY_MASK = false; +const DEFAULT_OUTPUT_CONFIDENCE_MASKS = true; // The OSS JS API does not support the builder pattern. // tslint:disable:jspb-use-builder-pattern +/** + * A callback that receives the computed masks from the interactive segmenter. + * The returned data is only valid for the duration of the callback. If + * asynchronous processing is needed, all data needs to be copied before the + * callback returns. + */ +export type InteractiveSegmenterCallack = + (result: InteractiveSegmenterResult) => void; + /** * Performs interactive segmentation on images. * @@ -69,7 +83,9 @@ const IMAGEA_SEGMENTER_GRAPH = * - batch is always 1 */ export class InteractiveSegmenter extends VisionTaskRunner { - private userCallback: SegmentationMaskCallback = () => {}; + private result: InteractiveSegmenterResult = {width: 0, height: 0}; + private outputCategoryMask = DEFAULT_OUTPUT_CATEGORY_MASK; + private outputConfidenceMasks = DEFAULT_OUTPUT_CONFIDENCE_MASKS; private readonly options: ImageSegmenterGraphOptionsProto; private readonly segmenterOptions: SegmenterOptionsProto; @@ -154,12 +170,14 @@ export class InteractiveSegmenter extends VisionTaskRunner { * @return A Promise that resolves when the settings have been applied. */ override setOptions(options: InteractiveSegmenterOptions): Promise { - if (options.outputType === 'CONFIDENCE_MASK') { - this.segmenterOptions.setOutputType( - SegmenterOptionsProto.OutputType.CONFIDENCE_MASK); - } else { - this.segmenterOptions.setOutputType( - SegmenterOptionsProto.OutputType.CATEGORY_MASK); + if ('outputCategoryMask' in options) { + this.outputCategoryMask = + options.outputCategoryMask ?? DEFAULT_OUTPUT_CATEGORY_MASK; + } + + if ('outputConfidenceMasks' in options) { + this.outputConfidenceMasks = + options.outputConfidenceMasks ?? DEFAULT_OUTPUT_CONFIDENCE_MASKS; } return super.applyOptions(options); @@ -184,7 +202,7 @@ export class InteractiveSegmenter extends VisionTaskRunner { */ segment( image: ImageSource, roi: RegionOfInterest, - callback: SegmentationMaskCallback): void; + callback: InteractiveSegmenterCallack): void; /** * Performs interactive segmentation on the provided single image and invokes * the callback with the response. The `roi` parameter is used to represent a @@ -213,24 +231,29 @@ export class InteractiveSegmenter extends VisionTaskRunner { segment( image: ImageSource, roi: RegionOfInterest, imageProcessingOptions: ImageProcessingOptions, - callback: SegmentationMaskCallback): void; + callback: InteractiveSegmenterCallack): void; segment( image: ImageSource, roi: RegionOfInterest, imageProcessingOptionsOrCallback: ImageProcessingOptions| - SegmentationMaskCallback, - callback?: SegmentationMaskCallback): void { + InteractiveSegmenterCallack, + callback?: InteractiveSegmenterCallack): void { const imageProcessingOptions = typeof imageProcessingOptionsOrCallback !== 'function' ? imageProcessingOptionsOrCallback : {}; - - this.userCallback = typeof imageProcessingOptionsOrCallback === 'function' ? + const userCallback = + typeof imageProcessingOptionsOrCallback === 'function' ? imageProcessingOptionsOrCallback : callback!; + this.reset(); this.processRenderData(roi, this.getSynctheticTimestamp()); this.processImageData(image, imageProcessingOptions); - this.userCallback = () => {}; + userCallback(this.result); + } + + private reset(): void { + this.result = {width: 0, height: 0}; } /** Updates the MediaPipe graph configuration. */ @@ -239,7 +262,6 @@ export class InteractiveSegmenter extends VisionTaskRunner { graphConfig.addInputStream(IMAGE_IN_STREAM); graphConfig.addInputStream(ROI_IN_STREAM); graphConfig.addInputStream(NORM_RECT_IN_STREAM); - graphConfig.addOutputStream(IMAGE_OUT_STREAM); const calculatorOptions = new CalculatorOptions(); calculatorOptions.setExtension( @@ -250,24 +272,47 @@ export class InteractiveSegmenter extends VisionTaskRunner { segmenterNode.addInputStream('IMAGE:' + IMAGE_IN_STREAM); segmenterNode.addInputStream('ROI:' + ROI_IN_STREAM); segmenterNode.addInputStream('NORM_RECT:' + NORM_RECT_IN_STREAM); - segmenterNode.addOutputStream('GROUPED_SEGMENTATION:' + IMAGE_OUT_STREAM); segmenterNode.setOptions(calculatorOptions); graphConfig.addNode(segmenterNode); - this.graphRunner.attachImageVectorListener( - IMAGE_OUT_STREAM, (masks, timestamp) => { - if (masks.length === 0) { - this.userCallback([], 0, 0); - } else { - this.userCallback( - masks.map(m => m.data), masks[0].width, masks[0].height); - } - this.setLatestOutputTimestamp(timestamp); - }); - this.graphRunner.attachEmptyPacketListener(IMAGE_OUT_STREAM, timestamp => { - this.setLatestOutputTimestamp(timestamp); - }); + if (this.outputConfidenceMasks) { + graphConfig.addOutputStream(CONFIDENCE_MASKS_STREAM); + segmenterNode.addOutputStream( + 'CONFIDENCE_MASKS:' + CONFIDENCE_MASKS_STREAM); + + this.graphRunner.attachImageVectorListener( + CONFIDENCE_MASKS_STREAM, (masks, timestamp) => { + this.result.confidenceMasks = masks.map(m => m.data); + if (masks.length >= 0) { + this.result.width = masks[0].width; + this.result.height = masks[0].height; + } + + this.setLatestOutputTimestamp(timestamp); + }); + this.graphRunner.attachEmptyPacketListener( + CONFIDENCE_MASKS_STREAM, timestamp => { + this.setLatestOutputTimestamp(timestamp); + }); + } + + if (this.outputCategoryMask) { + graphConfig.addOutputStream(CATEGORY_MASK_STREAM); + segmenterNode.addOutputStream('CATEGORY_MASK:' + CATEGORY_MASK_STREAM); + + this.graphRunner.attachImageListener( + CATEGORY_MASK_STREAM, (mask, timestamp) => { + this.result.categoryMask = mask.data; + this.result.width = mask.width; + this.result.height = mask.height; + this.setLatestOutputTimestamp(timestamp); + }); + this.graphRunner.attachEmptyPacketListener( + CATEGORY_MASK_STREAM, timestamp => { + this.setLatestOutputTimestamp(timestamp); + }); + } const binaryGraph = graphConfig.serializeBinary(); this.setGraph(new Uint8Array(binaryGraph), /* isBinary= */ true); diff --git a/mediapipe/tasks/web/vision/interactive_segmenter/interactive_segmenter_options.d.ts b/mediapipe/tasks/web/vision/interactive_segmenter/interactive_segmenter_options.d.ts index beb43cd8..269403d9 100644 --- a/mediapipe/tasks/web/vision/interactive_segmenter/interactive_segmenter_options.d.ts +++ b/mediapipe/tasks/web/vision/interactive_segmenter/interactive_segmenter_options.d.ts @@ -19,18 +19,9 @@ import {TaskRunnerOptions} from '../../../../tasks/web/core/task_runner_options' /** Options to configure the MediaPipe Interactive Segmenter Task */ export interface InteractiveSegmenterOptions extends TaskRunnerOptions { - /** - * The output type of segmentation results. - * - * The two supported modes are: - * - Category Mask: Gives a single output mask where each pixel represents - * the class which the pixel in the original image was - * predicted to belong to. - * - Confidence Mask: Gives a list of output masks (one for each class). For - * each mask, the pixel represents the prediction - * confidence, usually in the [0.0, 0.1] range. - * - * Defaults to `CATEGORY_MASK`. - */ - outputType?: 'CATEGORY_MASK'|'CONFIDENCE_MASK'|undefined; + /** Whether to output confidence masks. Defaults to true. */ + outputConfidenceMasks?: boolean|undefined; + + /** Whether to output the category masks. Defaults to false. */ + outputCategoryMask?: boolean|undefined; } diff --git a/mediapipe/tasks/web/vision/interactive_segmenter/interactive_segmenter_result.d.ts b/mediapipe/tasks/web/vision/interactive_segmenter/interactive_segmenter_result.d.ts new file mode 100644 index 00000000..f7e1f3a1 --- /dev/null +++ b/mediapipe/tasks/web/vision/interactive_segmenter/interactive_segmenter_result.d.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2023 The MediaPipe Authors. All Rights Reserved. + * + * 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. + */ + +/** The output result of InteractiveSegmenter. */ +export declare interface InteractiveSegmenterResult { + /** + * Multiple masks as Float32Arrays or WebGLTextures where, for each mask, each + * pixel represents the prediction confidence, usually in the [0, 1] range. + */ + confidenceMasks?: Float32Array[]|WebGLTexture[]; + + /** + * A category mask as a Uint8ClampedArray or WebGLTexture where each + * pixel represents the class which the pixel in the original image was + * predicted to belong to. + */ + categoryMask?: Uint8ClampedArray|WebGLTexture; + + /** The width of the masks. */ + width: number; + + /** The height of the masks. */ + height: number; +} diff --git a/mediapipe/tasks/web/vision/interactive_segmenter/interactive_segmenter_test.ts b/mediapipe/tasks/web/vision/interactive_segmenter/interactive_segmenter_test.ts index d6e3a97a..884be032 100644 --- a/mediapipe/tasks/web/vision/interactive_segmenter/interactive_segmenter_test.ts +++ b/mediapipe/tasks/web/vision/interactive_segmenter/interactive_segmenter_test.ts @@ -18,7 +18,7 @@ import 'jasmine'; // Placeholder for internal dependency on encodeByteArray import {CalculatorGraphConfig} from '../../../../framework/calculator_pb'; -import {addJasmineCustomFloatEqualityTester, createSpyWasmModule, MediapipeTasksFake, SpyWasmModule, verifyGraph, verifyListenersRegistered} from '../../../../tasks/web/core/task_runner_test_utils'; +import {addJasmineCustomFloatEqualityTester, createSpyWasmModule, MediapipeTasksFake, SpyWasmModule, verifyGraph} from '../../../../tasks/web/core/task_runner_test_utils'; import {RenderData as RenderDataProto} from '../../../../util/render_data_pb'; import {WasmImage} from '../../../../web/graph_runner/graph_runner_image_lib'; @@ -37,7 +37,9 @@ class InteractiveSegmenterFake extends InteractiveSegmenter implements graph: CalculatorGraphConfig|undefined; fakeWasmModule: SpyWasmModule; - imageVectorListener: + categoryMaskListener: + ((images: WasmImage, timestamp: number) => void)|undefined; + confidenceMasksListener: ((images: WasmImage[], timestamp: number) => void)|undefined; lastRoi?: RenderDataProto; @@ -46,11 +48,16 @@ class InteractiveSegmenterFake extends InteractiveSegmenter implements this.fakeWasmModule = this.graphRunner.wasmModule as unknown as SpyWasmModule; - this.attachListenerSpies[0] = + this.attachListenerSpies[0] = spyOn(this.graphRunner, 'attachImageListener') + .and.callFake((stream, listener) => { + expect(stream).toEqual('category_mask'); + this.categoryMaskListener = listener; + }); + this.attachListenerSpies[1] = spyOn(this.graphRunner, 'attachImageVectorListener') .and.callFake((stream, listener) => { - expect(stream).toEqual('image_out'); - this.imageVectorListener = listener; + expect(stream).toEqual('confidence_masks'); + this.confidenceMasksListener = listener; }); spyOn(this.graphRunner, 'setGraph').and.callFake(binaryGraph => { this.graph = CalculatorGraphConfig.deserializeBinary(binaryGraph); @@ -79,17 +86,21 @@ describe('InteractiveSegmenter', () => { it('initializes graph', async () => { verifyGraph(interactiveSegmenter); - verifyListenersRegistered(interactiveSegmenter); + + // Verify default options + expect(interactiveSegmenter.categoryMaskListener).not.toBeDefined(); + expect(interactiveSegmenter.confidenceMasksListener).toBeDefined(); }); it('reloads graph when settings are changed', async () => { - await interactiveSegmenter.setOptions({outputType: 'CATEGORY_MASK'}); - verifyGraph(interactiveSegmenter, [['segmenterOptions', 'outputType'], 1]); - verifyListenersRegistered(interactiveSegmenter); + await interactiveSegmenter.setOptions( + {outputConfidenceMasks: true, outputCategoryMask: false}); + expect(interactiveSegmenter.categoryMaskListener).not.toBeDefined(); + expect(interactiveSegmenter.confidenceMasksListener).toBeDefined(); - await interactiveSegmenter.setOptions({outputType: 'CONFIDENCE_MASK'}); - verifyGraph(interactiveSegmenter, [['segmenterOptions', 'outputType'], 2]); - verifyListenersRegistered(interactiveSegmenter); + await interactiveSegmenter.setOptions( + {outputConfidenceMasks: false, outputCategoryMask: true}); + expect(interactiveSegmenter.categoryMaskListener).toBeDefined(); }); it('can use custom models', async () => { @@ -115,23 +126,6 @@ describe('InteractiveSegmenter', () => { ]); }); - - describe('setOptions()', () => { - const fieldPath = ['segmenterOptions', 'outputType']; - - it(`can set outputType`, async () => { - await interactiveSegmenter.setOptions({outputType: 'CONFIDENCE_MASK'}); - verifyGraph(interactiveSegmenter, [fieldPath, 2]); - }); - - it(`can clear outputType`, async () => { - await interactiveSegmenter.setOptions({outputType: 'CONFIDENCE_MASK'}); - verifyGraph(interactiveSegmenter, [fieldPath, 2]); - await interactiveSegmenter.setOptions({outputType: undefined}); - verifyGraph(interactiveSegmenter, [fieldPath, 1]); - }); - }); - it('doesn\'t support region of interest', () => { expect(() => { interactiveSegmenter.segment( @@ -153,60 +147,99 @@ describe('InteractiveSegmenter', () => { interactiveSegmenter.segment({} as HTMLImageElement, ROI, () => {}); }); - it('supports category masks', (done) => { + it('supports category mask', async () => { const mask = new Uint8ClampedArray([1, 2, 3, 4]); + await interactiveSegmenter.setOptions( + {outputCategoryMask: true, outputConfidenceMasks: false}); + // Pass the test data to our listener interactiveSegmenter.fakeWasmModule._waitUntilIdle.and.callFake(() => { - verifyListenersRegistered(interactiveSegmenter); - interactiveSegmenter.imageVectorListener!( - [ - {data: mask, width: 2, height: 2}, - ], - /* timestamp= */ 1337); + expect(interactiveSegmenter.categoryMaskListener).toBeDefined(); + interactiveSegmenter.categoryMaskListener! + ({data: mask, width: 2, height: 2}, + /* timestamp= */ 1337); }); // Invoke the image segmenter - interactiveSegmenter.segment( - {} as HTMLImageElement, ROI, (masks, width, height) => { - expect(interactiveSegmenter.fakeWasmModule._waitUntilIdle) - .toHaveBeenCalled(); - expect(masks).toHaveSize(1); - expect(masks[0]).toEqual(mask); - expect(width).toEqual(2); - expect(height).toEqual(2); - done(); - }); + return new Promise(resolve => { + interactiveSegmenter.segment({} as HTMLImageElement, ROI, result => { + expect(interactiveSegmenter.fakeWasmModule._waitUntilIdle) + .toHaveBeenCalled(); + expect(result.categoryMask).toEqual(mask); + expect(result.confidenceMasks).not.toBeDefined(); + expect(result.width).toEqual(2); + expect(result.height).toEqual(2); + resolve(); + }); + }); }); it('supports confidence masks', async () => { const mask1 = new Float32Array([0.1, 0.2, 0.3, 0.4]); const mask2 = new Float32Array([0.5, 0.6, 0.7, 0.8]); - await interactiveSegmenter.setOptions({outputType: 'CONFIDENCE_MASK'}); + await interactiveSegmenter.setOptions( + {outputCategoryMask: false, outputConfidenceMasks: true}); // Pass the test data to our listener interactiveSegmenter.fakeWasmModule._waitUntilIdle.and.callFake(() => { - verifyListenersRegistered(interactiveSegmenter); - interactiveSegmenter.imageVectorListener!( + expect(interactiveSegmenter.confidenceMasksListener).toBeDefined(); + interactiveSegmenter.confidenceMasksListener!( [ {data: mask1, width: 2, height: 2}, {data: mask2, width: 2, height: 2}, ], 1337); }); + return new Promise(resolve => { + // Invoke the image segmenter + interactiveSegmenter.segment({} as HTMLImageElement, ROI, result => { + expect(interactiveSegmenter.fakeWasmModule._waitUntilIdle) + .toHaveBeenCalled(); + expect(result.categoryMask).not.toBeDefined(); + expect(result.confidenceMasks).toEqual([mask1, mask2]); + expect(result.width).toEqual(2); + expect(result.height).toEqual(2); + resolve(); + }); + }); + }); + + it('supports combined category and confidence masks', async () => { + const categoryMask = new Uint8ClampedArray([1, 0]); + const confidenceMask1 = new Float32Array([0.0, 1.0]); + const confidenceMask2 = new Float32Array([1.0, 0.0]); + + await interactiveSegmenter.setOptions( + {outputCategoryMask: true, outputConfidenceMasks: true}); + + // Pass the test data to our listener + interactiveSegmenter.fakeWasmModule._waitUntilIdle.and.callFake(() => { + expect(interactiveSegmenter.categoryMaskListener).toBeDefined(); + expect(interactiveSegmenter.confidenceMasksListener).toBeDefined(); + interactiveSegmenter.categoryMaskListener! + ({data: categoryMask, width: 1, height: 1}, 1337); + interactiveSegmenter.confidenceMasksListener!( + [ + {data: confidenceMask1, width: 1, height: 1}, + {data: confidenceMask2, width: 1, height: 1}, + ], + 1337); + }); return new Promise(resolve => { // Invoke the image segmenter interactiveSegmenter.segment( - {} as HTMLImageElement, ROI, (masks, width, height) => { + {} as HTMLImageElement, ROI, result => { expect(interactiveSegmenter.fakeWasmModule._waitUntilIdle) .toHaveBeenCalled(); - expect(masks).toHaveSize(2); - expect(masks[0]).toEqual(mask1); - expect(masks[1]).toEqual(mask2); - expect(width).toEqual(2); - expect(height).toEqual(2); + expect(result.categoryMask).toEqual(categoryMask); + expect(result.confidenceMasks).toEqual([ + confidenceMask1, confidenceMask2 + ]); + expect(result.width).toEqual(1); + expect(result.height).toEqual(1); resolve(); }); }); From 2564fec44c850cbf4c0bc61edb4e8ba391087499 Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Mon, 17 Apr 2023 13:56:11 -0700 Subject: [PATCH 22/30] Internal MediaPipe Tasks change. PiperOrigin-RevId: 524942203 --- mediapipe/tasks/cc/core/BUILD | 1 + .../cc/core/mediapipe_builtin_op_resolver.cc | 3 + .../cc/text/custom_ops/sentencepiece/BUILD | 20 +++ .../sentencepiece_tokenizer_tflite.cc | 129 ++++++++++++++++++ .../sentencepiece_tokenizer_tflite.h | 27 ++++ .../text/text_embedder/text_embedder_test.cc | 57 ++++++++ 6 files changed, 237 insertions(+) create mode 100644 mediapipe/tasks/cc/text/custom_ops/sentencepiece/sentencepiece_tokenizer_tflite.cc create mode 100644 mediapipe/tasks/cc/text/custom_ops/sentencepiece/sentencepiece_tokenizer_tflite.h diff --git a/mediapipe/tasks/cc/core/BUILD b/mediapipe/tasks/cc/core/BUILD index 7b2b9778..5aa9c972 100644 --- a/mediapipe/tasks/cc/core/BUILD +++ b/mediapipe/tasks/cc/core/BUILD @@ -78,6 +78,7 @@ cc_library( hdrs = ["mediapipe_builtin_op_resolver.h"], deps = [ "//mediapipe/tasks/cc/text/custom_ops/ragged:ragged_tensor_to_tensor_tflite", + "//mediapipe/tasks/cc/text/custom_ops/sentencepiece:sentencepiece_tokenizer_tflite", "//mediapipe/tasks/cc/text/language_detector/custom_ops:kmeans_embedding_lookup", "//mediapipe/tasks/cc/text/language_detector/custom_ops:ngram_hash", "//mediapipe/util/tflite/operations:landmarks_to_transform_matrix", diff --git a/mediapipe/tasks/cc/core/mediapipe_builtin_op_resolver.cc b/mediapipe/tasks/cc/core/mediapipe_builtin_op_resolver.cc index ae64e33e..80097fd0 100644 --- a/mediapipe/tasks/cc/core/mediapipe_builtin_op_resolver.cc +++ b/mediapipe/tasks/cc/core/mediapipe_builtin_op_resolver.cc @@ -16,6 +16,7 @@ limitations under the License. #include "mediapipe/tasks/cc/core/mediapipe_builtin_op_resolver.h" #include "mediapipe/tasks/cc/text/custom_ops/ragged/ragged_tensor_to_tensor_tflite.h" +#include "mediapipe/tasks/cc/text/custom_ops/sentencepiece/sentencepiece_tokenizer_tflite.h" #include "mediapipe/tasks/cc/text/language_detector/custom_ops/kmeans_embedding_lookup.h" #include "mediapipe/tasks/cc/text/language_detector/custom_ops/ngram_hash.h" #include "mediapipe/util/tflite/operations/landmarks_to_transform_matrix.h" @@ -51,6 +52,8 @@ MediaPipeBuiltinOpResolver::MediaPipeBuiltinOpResolver() { AddCustom("KmeansEmbeddingLookup", mediapipe::tflite_operations::Register_KmeansEmbeddingLookup()); // For the UniversalSentenceEncoder model. + AddCustom("TFSentencepieceTokenizeOp", + mediapipe::tflite_operations::Register_SENTENCEPIECE_TOKENIZER()); AddCustom("RaggedTensorToTensor", mediapipe::tflite_operations::Register_RAGGED_TENSOR_TO_TENSOR()); } diff --git a/mediapipe/tasks/cc/text/custom_ops/sentencepiece/BUILD b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/BUILD index a1833ac5..19f843c4 100644 --- a/mediapipe/tasks/cc/text/custom_ops/sentencepiece/BUILD +++ b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/BUILD @@ -127,6 +127,26 @@ cc_library( ], ) +cc_library( + name = "sentencepiece_tokenizer_tflite", + srcs = ["sentencepiece_tokenizer_tflite.cc"], + hdrs = ["sentencepiece_tokenizer_tflite.h"], + visibility = [ + "//visibility:public", + ], + deps = + [ + ":optimized_encoder", + "@flatbuffers", + "@org_tensorflow//tensorflow/lite:framework", + "@org_tensorflow//tensorflow/lite:string_util", + "@org_tensorflow//tensorflow/lite/c:common", + "@org_tensorflow//tensorflow/lite/kernels:builtin_ops", + "@org_tensorflow//tensorflow/lite/kernels:kernel_util", + "@org_tensorflow//tensorflow/lite/kernels/internal:tensor", + ], +) + cc_test( name = "optimized_encoder_test", srcs = [ diff --git a/mediapipe/tasks/cc/text/custom_ops/sentencepiece/sentencepiece_tokenizer_tflite.cc b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/sentencepiece_tokenizer_tflite.cc new file mode 100644 index 00000000..468a3a54 --- /dev/null +++ b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/sentencepiece_tokenizer_tflite.cc @@ -0,0 +1,129 @@ +/* Copyright 2023 The MediaPipe Authors. All Rights Reserved. + +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/tasks/cc/text/custom_ops/sentencepiece/sentencepiece_tokenizer_tflite.h" + +#include "flatbuffers/flexbuffers.h" +#include "mediapipe/tasks/cc/text/custom_ops/sentencepiece/optimized_encoder.h" +#include "tensorflow/lite/c/common.h" +#include "tensorflow/lite/context.h" +#include "tensorflow/lite/kernels/internal/tensor.h" +#include "tensorflow/lite/kernels/kernel_util.h" +#include "tensorflow/lite/model.h" +#include "tensorflow/lite/string_util.h" + +namespace mediapipe::tflite_operations { +namespace sentencepiece::tokenizer { +namespace { + +using ::tflite::SetTensorToDynamic; + +constexpr int kSPModelIndex = 0; +constexpr int kInputIndex = 1; +constexpr int kAddBOSInput = 4; +constexpr int kAddEOSInput = 5; +constexpr int kReverseInput = 6; + +constexpr int kOutputValuesInd = 0; +constexpr int kOutputSplitsInd = 1; + +TfLiteIntArray* CreateSizeArray(const std::initializer_list& sizes) { + TfLiteIntArray* array_size = TfLiteIntArrayCreate(sizes.size()); + int index = 0; + for (const int size : sizes) { + array_size->data[index++] = size; + } + return array_size; +} +} // namespace + +// Initializes text encoder object from serialized parameters. +void* Initialize(TfLiteContext* /*context*/, const char* /*buffer*/, + size_t /*length*/) { + return nullptr; +} +void Free(TfLiteContext* /*context*/, void* /*buffer*/) {} + +TfLiteStatus Prepare(TfLiteContext* context, TfLiteNode* node) { + // TODO: Add checks for input and output tensors. + TfLiteTensor& output_values = + context->tensors[node->outputs->data[kOutputValuesInd]]; + SetTensorToDynamic(&output_values); + + TfLiteTensor& output_splits = + context->tensors[node->outputs->data[kOutputSplitsInd]]; + SetTensorToDynamic(&output_splits); + return kTfLiteOk; +} + +TfLiteStatus Eval(TfLiteContext* context, TfLiteNode* node) { + const TfLiteTensor& model_tensor = + context->tensors[node->inputs->data[kSPModelIndex]]; + const auto model_buffer_data = model_tensor.data.data; + const TfLiteTensor& input_text = + context->tensors[node->inputs->data[kInputIndex]]; + + const TfLiteTensor add_bos_tensor = + context->tensors[node->inputs->data[kAddBOSInput]]; + const bool add_bos = add_bos_tensor.data.b[0]; + const TfLiteTensor add_eos_tensor = + context->tensors[node->inputs->data[kAddEOSInput]]; + const bool add_eos = add_eos_tensor.data.b[0]; + const TfLiteTensor reverse_tensor = + context->tensors[node->inputs->data[kReverseInput]]; + const bool reverse = reverse_tensor.data.b[0]; + + std::vector encoded; + std::vector splits; + const int num_strings = tflite::GetStringCount(&input_text); + for (int i = 0; i < num_strings; ++i) { + const auto strref = tflite::GetString(&input_text, i); + const auto res = EncodeString(std::string(strref.str, strref.len), + model_buffer_data, add_bos, add_eos, reverse); + TF_LITE_ENSURE_MSG(context, res.type == EncoderResultType::SUCCESS, + "Sentencepiece conversion failed"); + std::copy(res.codes.begin(), res.codes.end(), std::back_inserter(encoded)); + splits.emplace_back(encoded.size()); + } + + TfLiteTensor& output_values = + context->tensors[node->outputs->data[kOutputValuesInd]]; + TF_LITE_ENSURE_OK(context, + context->ResizeTensor( + context, &output_values, + CreateSizeArray({static_cast(encoded.size())}))); + int32_t* output_values_flat = output_values.data.i32; + std::copy(encoded.begin(), encoded.end(), output_values_flat); + TfLiteTensor& output_splits = + context->tensors[node->outputs->data[kOutputSplitsInd]]; + TF_LITE_ENSURE_OK( + context, context->ResizeTensor( + context, &output_splits, + CreateSizeArray({static_cast(splits.size() + 1)}))); + int32_t* output_splits_flat = output_splits.data.i32; + *output_splits_flat = 0; + std::copy(splits.begin(), splits.end(), output_splits_flat + 1); + return kTfLiteOk; +} +} // namespace sentencepiece::tokenizer + +TfLiteRegistration* Register_SENTENCEPIECE_TOKENIZER() { + static TfLiteRegistration r = { + sentencepiece::tokenizer::Initialize, sentencepiece::tokenizer::Free, + sentencepiece::tokenizer::Prepare, sentencepiece::tokenizer::Eval}; + return &r; +} + +} // namespace mediapipe::tflite_operations diff --git a/mediapipe/tasks/cc/text/custom_ops/sentencepiece/sentencepiece_tokenizer_tflite.h b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/sentencepiece_tokenizer_tflite.h new file mode 100644 index 00000000..8a9fa8ae --- /dev/null +++ b/mediapipe/tasks/cc/text/custom_ops/sentencepiece/sentencepiece_tokenizer_tflite.h @@ -0,0 +1,27 @@ +/* Copyright 2023 The MediaPipe Authors. All Rights Reserved. + +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_TASKS_CC_TEXT_CUSTOM_OPS_SENTENCEPIECE_SENTENCEPIECE_TOKENIZER_TFLITE_H_ +#define MEDIAPIPE_TASKS_CC_TEXT_CUSTOM_OPS_SENTENCEPIECE_SENTENCEPIECE_TOKENIZER_TFLITE_H_ + +#include "tensorflow/lite/kernels/register.h" + +namespace mediapipe::tflite_operations { + +TfLiteRegistration* Register_SENTENCEPIECE_TOKENIZER(); + +} // namespace mediapipe::tflite_operations + +#endif // MEDIAPIPE_TASKS_CC_TEXT_CUSTOM_OPS_SENTENCEPIECE_SENTENCEPIECE_TOKENIZER_TFLITE_H_ diff --git a/mediapipe/tasks/cc/text/text_embedder/text_embedder_test.cc b/mediapipe/tasks/cc/text/text_embedder/text_embedder_test.cc index 5e0be557..474f0ca3 100644 --- a/mediapipe/tasks/cc/text/text_embedder/text_embedder_test.cc +++ b/mediapipe/tasks/cc/text/text_embedder/text_embedder_test.cc @@ -39,6 +39,8 @@ constexpr char kMobileBert[] = "mobilebert_embedding_with_metadata.tflite"; // Embedding model with regex preprocessing. constexpr char kRegexOneEmbeddingModel[] = "regex_one_embedding_with_metadata.tflite"; +constexpr char kUniversalSentenceEncoderModel[] = + "universal_sentence_encoder_qa_with_metadata.tflite"; // Tolerance for embedding vector coordinate values. constexpr float kEpsilon = 1e-4; @@ -147,6 +149,35 @@ TEST_F(EmbedderTest, SucceedsWithQuantization) { MP_ASSERT_OK(text_embedder->Close()); } +TEST(EmbedTest, SucceedsWithUniversalSentenceEncoderModel) { + auto options = std::make_unique(); + options->base_options.model_asset_path = + JoinPath("./", kTestDataDirectory, kUniversalSentenceEncoderModel); + MP_ASSERT_OK_AND_ASSIGN(std::unique_ptr text_embedder, + TextEmbedder::Create(std::move(options))); + + MP_ASSERT_OK_AND_ASSIGN( + auto result0, + text_embedder->Embed("it's a charming and often affecting journey")); + ASSERT_EQ(result0.embeddings.size(), 1); + ASSERT_EQ(result0.embeddings[0].float_embedding.size(), 100); + ASSERT_NEAR(result0.embeddings[0].float_embedding[0], 1.422951f, kEpsilon); + + MP_ASSERT_OK_AND_ASSIGN( + auto result1, text_embedder->Embed("what a great and fantastic trip")); + ASSERT_EQ(result1.embeddings.size(), 1); + ASSERT_EQ(result1.embeddings[0].float_embedding.size(), 100); + ASSERT_NEAR(result1.embeddings[0].float_embedding[0], 1.404664f, kEpsilon); + + // Check cosine similarity. + MP_ASSERT_OK_AND_ASSIGN( + double similarity, TextEmbedder::CosineSimilarity(result0.embeddings[0], + result1.embeddings[0])); + ASSERT_NEAR(similarity, 0.851961, kSimilarityTolerancy); + + MP_ASSERT_OK(text_embedder->Close()); +} + TEST_F(EmbedderTest, SucceedsWithMobileBertAndDifferentThemes) { auto options = std::make_unique(); options->base_options.model_asset_path = @@ -178,5 +209,31 @@ TEST_F(EmbedderTest, SucceedsWithMobileBertAndDifferentThemes) { MP_ASSERT_OK(text_embedder->Close()); } +TEST_F(EmbedderTest, SucceedsWithUSEAndDifferentThemes) { + auto options = std::make_unique(); + options->base_options.model_asset_path = + JoinPath("./", kTestDataDirectory, kUniversalSentenceEncoderModel); + MP_ASSERT_OK_AND_ASSIGN(std::unique_ptr text_embedder, + TextEmbedder::Create(std::move(options))); + + MP_ASSERT_OK_AND_ASSIGN( + TextEmbedderResult result0, + text_embedder->Embed("When you go to this restaurant, they hold the " + "pancake upside-down before they hand it " + "to you. It's a great gimmick.")); + MP_ASSERT_OK_AND_ASSIGN( + TextEmbedderResult result1, + text_embedder->Embed( + "Let's make a plan to steal the declaration of independence.")); + + // Check cosine similarity. + MP_ASSERT_OK_AND_ASSIGN( + double similarity, TextEmbedder::CosineSimilarity(result0.embeddings[0], + result1.embeddings[0])); + EXPECT_NEAR(similarity, 0.780334, kSimilarityTolerancy); + + MP_ASSERT_OK(text_embedder->Close()); +} + } // namespace } // namespace mediapipe::tasks::text::text_embedder From 47e55fcf2f5f6ef588088a828289138862a2f627 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Mon, 17 Apr 2023 14:29:18 -0700 Subject: [PATCH 23/30] Add HAND_CONNECTIONS to HandLandmarker and GestureRecognizer PiperOrigin-RevId: 524951052 --- .../face_landmarks_connections.ts | 2 +- .../tasks/web/vision/gesture_recognizer/BUILD | 1 + .../gesture_recognizer/gesture_recognizer.ts | 7 +++++ .../tasks/web/vision/hand_landmarker/BUILD | 7 +++++ .../vision/hand_landmarker/hand_landmarker.ts | 7 +++++ .../hand_landmarks_connections.ts | 31 +++++++++++++++++++ 6 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 mediapipe/tasks/web/vision/hand_landmarker/hand_landmarks_connections.ts diff --git a/mediapipe/tasks/web/vision/face_landmarker/face_landmarks_connections.ts b/mediapipe/tasks/web/vision/face_landmarker/face_landmarks_connections.ts index 97832475..337f663e 100644 --- a/mediapipe/tasks/web/vision/face_landmarker/face_landmarks_connections.ts +++ b/mediapipe/tasks/web/vision/face_landmarker/face_landmarks_connections.ts @@ -19,7 +19,7 @@ import {Connection} from '../../../../tasks/web/vision/core/types'; // tslint:disable:class-as-namespace Using for easier import by 3P users /** - * A class containing the Pairs of landmark indices to be rendered with + * A class containing the pairs of landmark indices to be rendered with * connections. */ export class FaceLandmarksConnections { diff --git a/mediapipe/tasks/web/vision/gesture_recognizer/BUILD b/mediapipe/tasks/web/vision/gesture_recognizer/BUILD index 9156e89b..a3a630e9 100644 --- a/mediapipe/tasks/web/vision/gesture_recognizer/BUILD +++ b/mediapipe/tasks/web/vision/gesture_recognizer/BUILD @@ -34,6 +34,7 @@ mediapipe_ts_library( "//mediapipe/tasks/web/core:classifier_options", "//mediapipe/tasks/web/vision/core:image_processing_options", "//mediapipe/tasks/web/vision/core:vision_task_runner", + "//mediapipe/tasks/web/vision/hand_landmarker:hand_landmarks_connections", "//mediapipe/web/graph_runner:graph_runner_ts", ], ) diff --git a/mediapipe/tasks/web/vision/gesture_recognizer/gesture_recognizer.ts b/mediapipe/tasks/web/vision/gesture_recognizer/gesture_recognizer.ts index 74d37cb6..df9c9128 100644 --- a/mediapipe/tasks/web/vision/gesture_recognizer/gesture_recognizer.ts +++ b/mediapipe/tasks/web/vision/gesture_recognizer/gesture_recognizer.ts @@ -31,6 +31,7 @@ import {convertClassifierOptionsToProto} from '../../../../tasks/web/components/ import {WasmFileset} from '../../../../tasks/web/core/wasm_fileset'; import {ImageProcessingOptions} from '../../../../tasks/web/vision/core/image_processing_options'; import {VisionGraphRunner, VisionTaskRunner} from '../../../../tasks/web/vision/core/vision_task_runner'; +import {HAND_CONNECTIONS} from '../../../../tasks/web/vision/hand_landmarker/hand_landmarks_connections'; import {ImageSource, WasmModule} from '../../../../web/graph_runner/graph_runner'; // Placeholder for internal dependency on trusted resource url @@ -72,6 +73,12 @@ export class GestureRecognizer extends VisionTaskRunner { private readonly handGestureRecognizerGraphOptions: HandGestureRecognizerGraphOptions; + /** + * An array containing the pairs of hand landmark indices to be rendered with + * connections. + */ + static HAND_CONNECTIONS = HAND_CONNECTIONS; + /** * Initializes the Wasm runtime and creates a new gesture recognizer from the * provided options. diff --git a/mediapipe/tasks/web/vision/hand_landmarker/BUILD b/mediapipe/tasks/web/vision/hand_landmarker/BUILD index c5687ee2..948d5692 100644 --- a/mediapipe/tasks/web/vision/hand_landmarker/BUILD +++ b/mediapipe/tasks/web/vision/hand_landmarker/BUILD @@ -16,6 +16,7 @@ mediapipe_ts_library( visibility = ["//visibility:public"], deps = [ ":hand_landmarker_types", + ":hand_landmarks_connections", "//mediapipe/framework:calculator_jspb_proto", "//mediapipe/framework:calculator_options_jspb_proto", "//mediapipe/framework/formats:classification_jspb_proto", @@ -72,3 +73,9 @@ jasmine_node_test( tags = ["nomsan"], deps = [":hand_landmarker_test_lib"], ) + +mediapipe_ts_library( + name = "hand_landmarks_connections", + srcs = ["hand_landmarks_connections.ts"], + deps = ["//mediapipe/tasks/web/vision/core:types"], +) diff --git a/mediapipe/tasks/web/vision/hand_landmarker/hand_landmarker.ts b/mediapipe/tasks/web/vision/hand_landmarker/hand_landmarker.ts index 1978bb06..62928536 100644 --- a/mediapipe/tasks/web/vision/hand_landmarker/hand_landmarker.ts +++ b/mediapipe/tasks/web/vision/hand_landmarker/hand_landmarker.ts @@ -27,6 +27,7 @@ import {Landmark, NormalizedLandmark} from '../../../../tasks/web/components/con import {WasmFileset} from '../../../../tasks/web/core/wasm_fileset'; import {ImageProcessingOptions} from '../../../../tasks/web/vision/core/image_processing_options'; import {VisionGraphRunner, VisionTaskRunner} from '../../../../tasks/web/vision/core/vision_task_runner'; +import {HAND_CONNECTIONS} from '../../../../tasks/web/vision/hand_landmarker/hand_landmarks_connections'; import {ImageSource, WasmModule} from '../../../../web/graph_runner/graph_runner'; // Placeholder for internal dependency on trusted resource url @@ -63,6 +64,12 @@ export class HandLandmarker extends VisionTaskRunner { HandLandmarksDetectorGraphOptions; private readonly handDetectorGraphOptions: HandDetectorGraphOptions; + /** + * An array containing the pairs of hand landmark indices to be rendered with + * connections. + */ + static HAND_CONNECTIONS = HAND_CONNECTIONS; + /** * Initializes the Wasm runtime and creates a new `HandLandmarker` from the * provided options. diff --git a/mediapipe/tasks/web/vision/hand_landmarker/hand_landmarks_connections.ts b/mediapipe/tasks/web/vision/hand_landmarker/hand_landmarks_connections.ts new file mode 100644 index 00000000..edb789c8 --- /dev/null +++ b/mediapipe/tasks/web/vision/hand_landmarker/hand_landmarks_connections.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2023 The MediaPipe Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Connection} from '../../../../tasks/web/vision/core/types'; + +/** + * An array containing the pairs of hand landmark indices to be rendered with + * connections. + */ +export const HAND_CONNECTIONS: Connection[] = [ + {start: 0, end: 1}, {start: 1, end: 2}, {start: 2, end: 3}, + {start: 3, end: 4}, {start: 0, end: 5}, {start: 5, end: 6}, + {start: 6, end: 7}, {start: 7, end: 8}, {start: 5, end: 9}, + {start: 9, end: 10}, {start: 10, end: 11}, {start: 11, end: 12}, + {start: 9, end: 13}, {start: 13, end: 14}, {start: 14, end: 15}, + {start: 15, end: 16}, {start: 13, end: 17}, {start: 0, end: 17}, + {start: 17, end: 18}, {start: 18, end: 19}, {start: 19, end: 20} +]; From 43fd7442963de728a670819b99bb56a680facc23 Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Mon, 17 Apr 2023 17:41:19 -0700 Subject: [PATCH 24/30] Internal Changes PiperOrigin-RevId: 524997017 --- .../python/text/text_classifier/text_classifier_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mediapipe/model_maker/python/text/text_classifier/text_classifier_test.py b/mediapipe/model_maker/python/text/text_classifier/text_classifier_test.py index 0a7e7a0e..6aa68a28 100644 --- a/mediapipe/model_maker/python/text/text_classifier/text_classifier_test.py +++ b/mediapipe/model_maker/python/text/text_classifier/text_classifier_test.py @@ -16,6 +16,7 @@ import csv import filecmp import os import tempfile +import unittest from unittest import mock as unittest_mock import tensorflow as tf @@ -24,6 +25,7 @@ from mediapipe.model_maker.python.text import text_classifier from mediapipe.tasks.python.test import test_utils +@unittest.skip('b/275624089') class TextClassifierTest(tf.test.TestCase): _AVERAGE_WORD_EMBEDDING_JSON_FILE = ( From 63cd09951dfd7b93fbfea90e3920e884b4dda886 Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Mon, 17 Apr 2023 21:15:00 -0700 Subject: [PATCH 25/30] Internal change PiperOrigin-RevId: 525030969 --- mediapipe/util/annotation_renderer.cc | 4 ++-- mediapipe/util/image_test_utils.cc | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mediapipe/util/annotation_renderer.cc b/mediapipe/util/annotation_renderer.cc index 671f4750..5188da89 100644 --- a/mediapipe/util/annotation_renderer.cc +++ b/mediapipe/util/annotation_renderer.cc @@ -56,8 +56,8 @@ bool NormalizedtoPixelCoordinates(double normalized_x, double normalized_y, VLOG(1) << "Normalized coordinates must be between 0.0 and 1.0"; } - *x_px = static_cast(round(normalized_x * image_width)); - *y_px = static_cast(round(normalized_y * image_height)); + *x_px = static_cast(round(normalized_x * image_width)); + *y_px = static_cast(round(normalized_y * image_height)); return true; } diff --git a/mediapipe/util/image_test_utils.cc b/mediapipe/util/image_test_utils.cc index 81566698..77b75595 100644 --- a/mediapipe/util/image_test_utils.cc +++ b/mediapipe/util/image_test_utils.cc @@ -43,14 +43,14 @@ mediapipe::ImageFormat::Format GetImageFormat(int image_channels) { Packet MakeImageFramePacket(cv::Mat input, int timestamp) { ImageFrame input_image(GetImageFormat(input.channels()), input.cols, - input.rows, input.step, input.data, [](uint8*) {}); + input.rows, input.step, input.data, [](uint8_t*) {}); return MakePacket(std::move(input_image)).At(Timestamp(0)); } Packet MakeImagePacket(cv::Mat input, int timestamp) { mediapipe::Image input_image(std::make_shared( GetImageFormat(input.channels()), input.cols, input.rows, input.step, - input.data, [](uint8*) {})); + input.data, [](uint8_t*) {})); return MakePacket(std::move(input_image)).At(Timestamp(0)); } From b4e27c137e2c5c014c3e849fec7ec0ed92759fbf Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Tue, 18 Apr 2023 00:52:09 -0700 Subject: [PATCH 26/30] Internal change PiperOrigin-RevId: 525069421 --- .../objectron/calculators/frame_annotation_tracker.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mediapipe/modules/objectron/calculators/frame_annotation_tracker.cc b/mediapipe/modules/objectron/calculators/frame_annotation_tracker.cc index eebf8857..1685a4f6 100644 --- a/mediapipe/modules/objectron/calculators/frame_annotation_tracker.cc +++ b/mediapipe/modules/objectron/calculators/frame_annotation_tracker.cc @@ -24,8 +24,8 @@ namespace mediapipe { void FrameAnnotationTracker::AddDetectionResult( const FrameAnnotation& frame_annotation) { - const int64 time_us = - static_cast(std::round(frame_annotation.timestamp())); + const int64_t time_us = + static_cast(std::round(frame_annotation.timestamp())); for (const auto& object_annotation : frame_annotation.annotations()) { detected_objects_[time_us + object_annotation.object_id()] = object_annotation; @@ -37,7 +37,7 @@ FrameAnnotation FrameAnnotationTracker::ConsolidateTrackingResult( absl::flat_hash_set* cancel_object_ids) { CHECK(cancel_object_ids != nullptr); FrameAnnotation frame_annotation; - std::vector keys_to_be_deleted; + std::vector keys_to_be_deleted; for (const auto& detected_obj : detected_objects_) { const int object_id = detected_obj.second.object_id(); if (cancel_object_ids->contains(object_id)) { From 88a10de345926cab4432c4ec61339b48af28f87e Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Tue, 18 Apr 2023 02:12:28 -0700 Subject: [PATCH 27/30] Internal change PiperOrigin-RevId: 525084368 --- mediapipe/gpu/gl_texture_buffer.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mediapipe/gpu/gl_texture_buffer.cc b/mediapipe/gpu/gl_texture_buffer.cc index 69b9889c..f1497f74 100644 --- a/mediapipe/gpu/gl_texture_buffer.cc +++ b/mediapipe/gpu/gl_texture_buffer.cc @@ -64,7 +64,7 @@ std::unique_ptr GlTextureBuffer::Create( int actual_ws = image_frame.WidthStep(); int alignment = 0; std::unique_ptr temp; - const uint8* data = image_frame.PixelData(); + const uint8_t* data = image_frame.PixelData(); // Let's see if the pixel data is tightly aligned to one of the alignments // supported by OpenGL, preferring 4 if possible since it's the default. From 3e0ed2ced053faae2f078dd2d53052c2f578120f Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Tue, 18 Apr 2023 10:10:58 -0700 Subject: [PATCH 28/30] Internal Changes PiperOrigin-RevId: 525180095 --- .../python/vision/object_detector/BUILD | 6 +----- .../object_detector/object_detector_test.py | 17 +++++++---------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/mediapipe/model_maker/python/vision/object_detector/BUILD b/mediapipe/model_maker/python/vision/object_detector/BUILD index f3d4407d..b97d215d 100644 --- a/mediapipe/model_maker/python/vision/object_detector/BUILD +++ b/mediapipe/model_maker/python/vision/object_detector/BUILD @@ -175,11 +175,7 @@ py_test( data = [":testdata"], tags = ["requires-net:external"], deps = [ - ":dataset", - ":hyperparameters", - ":model_spec", - ":object_detector", - ":object_detector_options", + ":object_detector_import", "//mediapipe/tasks/python/test:test_utils", ], ) diff --git a/mediapipe/model_maker/python/vision/object_detector/object_detector_test.py b/mediapipe/model_maker/python/vision/object_detector/object_detector_test.py index df6b58a0..02f773e6 100644 --- a/mediapipe/model_maker/python/vision/object_detector/object_detector_test.py +++ b/mediapipe/model_maker/python/vision/object_detector/object_detector_test.py @@ -19,11 +19,7 @@ from unittest import mock as unittest_mock from absl.testing import parameterized import tensorflow as tf -from mediapipe.model_maker.python.vision.object_detector import dataset -from mediapipe.model_maker.python.vision.object_detector import hyperparameters -from mediapipe.model_maker.python.vision.object_detector import model_spec as ms -from mediapipe.model_maker.python.vision.object_detector import object_detector -from mediapipe.model_maker.python.vision.object_detector import object_detector_options +from mediapipe.model_maker.python.vision import object_detector from mediapipe.tasks.python.test import test_utils as task_test_utils @@ -33,7 +29,7 @@ class ObjectDetectorTest(tf.test.TestCase, parameterized.TestCase): super().setUp() dataset_folder = task_test_utils.get_test_data_path('coco_data') cache_dir = self.create_tempdir() - self.data = dataset.Dataset.from_coco_folder( + self.data = object_detector.Dataset.from_coco_folder( dataset_folder, cache_dir=cache_dir ) # Mock tempfile.gettempdir() to be unique for each test to avoid race @@ -48,15 +44,16 @@ class ObjectDetectorTest(tf.test.TestCase, parameterized.TestCase): self.addCleanup(mock_gettempdir.stop) def test_object_detector(self): - hparams = hyperparameters.HParams( + hparams = object_detector.HParams( epochs=1, batch_size=2, learning_rate=0.9, shuffle=False, export_dir=self.create_tempdir(), ) - options = object_detector_options.ObjectDetectorOptions( - supported_model=ms.SupportedModels.MOBILENET_V2, hparams=hparams + options = object_detector.ObjectDetectorOptions( + supported_model=object_detector.SupportedModels.MOBILENET_V2, + hparams=hparams, ) # Test `create`` model = object_detector.ObjectDetector.create( @@ -79,7 +76,7 @@ class ObjectDetectorTest(tf.test.TestCase, parameterized.TestCase): self.assertGreater(os.path.getsize(output_metadata_file), 0) # Test `quantization_aware_training` - qat_hparams = hyperparameters.QATHParams( + qat_hparams = object_detector.QATHParams( learning_rate=0.9, batch_size=2, epochs=1, From 64d1e74c208d04b999998dbb2560adc266280a7e Mon Sep 17 00:00:00 2001 From: MediaPipe Team Date: Tue, 18 Apr 2023 10:18:19 -0700 Subject: [PATCH 29/30] Internal MediaPipe Tasks change PiperOrigin-RevId: 525182282 --- .../python/test/text/text_embedder_test.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/mediapipe/tasks/python/test/text/text_embedder_test.py b/mediapipe/tasks/python/test/text/text_embedder_test.py index 78e98a1b..62d162f6 100644 --- a/mediapipe/tasks/python/test/text/text_embedder_test.py +++ b/mediapipe/tasks/python/test/text/text_embedder_test.py @@ -32,6 +32,7 @@ _TextEmbedderOptions = text_embedder.TextEmbedderOptions _BERT_MODEL_FILE = 'mobilebert_embedding_with_metadata.tflite' _REGEX_MODEL_FILE = 'regex_one_embedding_with_metadata.tflite' +_USE_MODEL_FILE = 'universal_sentence_encoder_qa_with_metadata.tflite' _TEST_DATA_DIR = 'mediapipe/tasks/testdata/text' # Tolerance for embedding vector coordinate values. _EPSILON = 1e-4 @@ -138,6 +139,24 @@ class TextEmbedderTest(parameterized.TestCase): 16, (0.549632, 0.552879), ), + ( + False, + False, + _USE_MODEL_FILE, + ModelFileType.FILE_NAME, + 0.851961, + 100, + (1.422951, 1.404664), + ), + ( + True, + False, + _USE_MODEL_FILE, + ModelFileType.FILE_CONTENT, + 0.851961, + 100, + (0.127049, 0.125416), + ), ) def test_embed(self, l2_normalize, quantize, model_name, model_file_type, expected_similarity, expected_size, expected_first_values): @@ -213,6 +232,24 @@ class TextEmbedderTest(parameterized.TestCase): 16, (0.549632, 0.552879), ), + ( + False, + False, + _USE_MODEL_FILE, + ModelFileType.FILE_NAME, + 0.851961, + 100, + (1.422951, 1.404664), + ), + ( + True, + False, + _USE_MODEL_FILE, + ModelFileType.FILE_CONTENT, + 0.851961, + 100, + (0.127049, 0.125416), + ), ) def test_embed_in_context(self, l2_normalize, quantize, model_name, model_file_type, expected_similarity, expected_size, @@ -251,6 +288,7 @@ class TextEmbedderTest(parameterized.TestCase): @parameterized.parameters( # TODO: The similarity should likely be lower (_BERT_MODEL_FILE, 0.980880), + (_USE_MODEL_FILE, 0.780334), ) def test_embed_with_different_themes(self, model_file, expected_similarity): # Creates embedder. From d7039c90dc9eebf047d099ee97c980c23c0c51ad Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Tue, 18 Apr 2023 14:02:43 -0700 Subject: [PATCH 30/30] Update WASM for Alpha 11 PiperOrigin-RevId: 525245471 --- third_party/wasm_files.bzl | 48 +++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/third_party/wasm_files.bzl b/third_party/wasm_files.bzl index 148b5970..a484d2f8 100644 --- a/third_party/wasm_files.bzl +++ b/third_party/wasm_files.bzl @@ -12,72 +12,72 @@ def wasm_files(): http_file( name = "com_google_mediapipe_wasm_audio_wasm_internal_js", - sha256 = "0eca68e2291a548b734bcab5db4c9e6b997e852ea7e19228003b9e2a78c7c646", - urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/audio_wasm_internal.js?generation=1681328323089931"], + sha256 = "b810de53d7ccf991b9c70fcdf7e88b5c3f2942ae766436f22be48159b6a7e687", + urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/audio_wasm_internal.js?generation=1681849488227617"], ) http_file( name = "com_google_mediapipe_wasm_audio_wasm_internal_wasm", - sha256 = "69bc95af5b783b510ec1842d6fb9594254907d8e1334799c5753164878a7dcac", - urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/audio_wasm_internal.wasm?generation=1681328325829340"], + sha256 = "26d91147e5c6c8a92e0a4ebf59599068a3cff6108847b793ef33ac23e98eddb9", + urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/audio_wasm_internal.wasm?generation=1681849491546937"], ) http_file( name = "com_google_mediapipe_wasm_audio_wasm_nosimd_internal_js", - sha256 = "88a0176cc80d6a1eb175a5105df705cf8b8684cf13f6db0a264af0b67b65a22a", - urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/audio_wasm_nosimd_internal.js?generation=1681328328330829"], + sha256 = "b38e37b3024692558eaaba159921fedd3297d1a09bba1c16a06fed327845b0bd", + urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/audio_wasm_nosimd_internal.js?generation=1681849494099698"], ) http_file( name = "com_google_mediapipe_wasm_audio_wasm_nosimd_internal_wasm", - sha256 = "1cc0c3db7d252801be4b090d8bbba61f308cc3dd5efe197319581d3af29495c7", - urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/audio_wasm_nosimd_internal.wasm?generation=1681328331085637"], + sha256 = "6a8e73d2e926565046e16adf1748f0f8ec5135fafe7eb8b9c83892e64c1a449a", + urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/audio_wasm_nosimd_internal.wasm?generation=1681849496451970"], ) http_file( name = "com_google_mediapipe_wasm_text_wasm_internal_js", - sha256 = "d9cd100b6d330d36f7749fe5fc64a2cdd0abb947a0376e6140784cfb0361a4e2", - urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/text_wasm_internal.js?generation=1681328333442454"], + sha256 = "785cba67b623b1dc66dc3621e97fd6b30edccbb408184a3094d0aa68ddd5becb", + urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/text_wasm_internal.js?generation=1681849498746265"], ) http_file( name = "com_google_mediapipe_wasm_text_wasm_internal_wasm", - sha256 = "30a2fcca630bdad6e99173ea7d0d8c5d7086aedf393d0159fa05bf9d08d4ff65", - urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/text_wasm_internal.wasm?generation=1681328335803336"], + sha256 = "a858b8a2e8b40e9c936b66566c5aefd396536c4e936459ab9ae7e239621adc14", + urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/text_wasm_internal.wasm?generation=1681849501370461"], ) http_file( name = "com_google_mediapipe_wasm_text_wasm_nosimd_internal_js", - sha256 = "70ca2bd15c56e0ce7bb10ff2188b4a1f9eafbb657eb9424e4cab8d7b29179871", - urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/text_wasm_nosimd_internal.js?generation=1681328338162884"], + sha256 = "5292f1442d5e5c037e7cffb78a8c2d71255348ca2c3bd759b314bdbedd5590c2", + urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/text_wasm_nosimd_internal.js?generation=1681849503379116"], ) http_file( name = "com_google_mediapipe_wasm_text_wasm_nosimd_internal_wasm", - sha256 = "8221b385905f36a769d7731a0adbe18b681bcb873561890429ca84278c67c3fd", - urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/text_wasm_nosimd_internal.wasm?generation=1681328340808115"], + sha256 = "e44b48ab29ee1d8befec804e9a63445c56266b679d19fb476d556ca621f0e493", + urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/text_wasm_nosimd_internal.wasm?generation=1681849505997020"], ) http_file( name = "com_google_mediapipe_wasm_vision_wasm_internal_js", - sha256 = "07692acd8202adafebd35dbcd7e2b8e88a76d4a0e6b9229cb3cad59503eeddc7", - urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/vision_wasm_internal.js?generation=1681328343147709"], + sha256 = "205855eba70464a92b9d00e90acac15c51a9f76192f900e697304ac6dea8f714", + urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/vision_wasm_internal.js?generation=1681849508414277"], ) http_file( name = "com_google_mediapipe_wasm_vision_wasm_internal_wasm", - sha256 = "03bf553fa6a768b0d70103a5e7d835b6b37371ff44e201c3392f22e0879737c3", - urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/vision_wasm_internal.wasm?generation=1681328345605574"], + sha256 = "c0cbd0df3adb2a9cd1331d14f522d2bae9f8adc9f1b35f92cbbc4b782b190cef", + urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/vision_wasm_internal.wasm?generation=1681849510936608"], ) http_file( name = "com_google_mediapipe_wasm_vision_wasm_nosimd_internal_js", - sha256 = "36697be14f921985eac15d1447ec8a260817b05ade1c9bb3ca7e906e0f047ec0", - urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/vision_wasm_nosimd_internal.js?generation=1681328348025082"], + sha256 = "0969812de4d3573198fa2eba4f5b0a7e97e98f97bd4215d876543f4925e57b84", + urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/vision_wasm_nosimd_internal.js?generation=1681849513292639"], ) http_file( name = "com_google_mediapipe_wasm_vision_wasm_nosimd_internal_wasm", - sha256 = "103fb145438d61cfecb2e8db3f06b43a5d77a7e3fcea940437fe272227cf2592", - urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/vision_wasm_nosimd_internal.wasm?generation=1681328350709881"], + sha256 = "f2ab62c3f8dabab0a573dadf5c105ff81a03c29c70f091f8cf273ae030c0a86f", + urls = ["https://storage.googleapis.com/mediapipe-assets/wasm/vision_wasm_nosimd_internal.wasm?generation=1681849515999000"], )