Initial upload: aesthetic-immich-starter
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# Copyright 2019 - 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import mediapipe.tasks.python as tasks
|
||||
from mediapipe.tasks.python.vision.core.image import Image
|
||||
from mediapipe.tasks.python.vision.core.image import ImageFormat
|
||||
|
||||
|
||||
__version__ = '0.10.35'
|
||||
@@ -0,0 +1,2 @@
|
||||
Left
|
||||
Right
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
???
|
||||
Bicycle
|
||||
Boot
|
||||
Laptop
|
||||
Person
|
||||
Chair
|
||||
Cattle
|
||||
Desk
|
||||
Cat
|
||||
Computer mouse
|
||||
Computer monitor
|
||||
Box
|
||||
Mug
|
||||
Coffee cup
|
||||
Stationary bicycle
|
||||
Table
|
||||
Bottle
|
||||
High heels
|
||||
Vehicle
|
||||
Footwear
|
||||
Dog
|
||||
Book
|
||||
Camera
|
||||
Car
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Copyright 2022 The MediaPipe Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
@@ -0,0 +1 @@
|
||||
"""Empty __init__.py file"""
|
||||
Binary file not shown.
+59
@@ -0,0 +1,59 @@
|
||||
// Copyright 2023 The MediaPipe Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
namespace mediapipe.tasks;
|
||||
|
||||
// Image segmenter metadata contains information specific for the image
|
||||
// segmentation task. The metadata can be added in
|
||||
// SubGraphMetadata.custom_metadata [1] in model metadata.
|
||||
// [1]: https://github.com/google/mediapipe/blob/46b5c4012d2ef76c9d92bb0d88a6b107aee83814/mediapipe/tasks/metadata/metadata_schema.fbs#L685
|
||||
|
||||
// ImageSegmenterOptions.min_parser_version indicates the minimum necessary
|
||||
// image segmenter metadata parser version to fully understand all fields in a
|
||||
// given metadata flatbuffer. This min_parser_version is specific for the
|
||||
// image segmenter metadata defined in this schema file.
|
||||
//
|
||||
// New fields and types will have associated comments with the schema version
|
||||
// for which they were added.
|
||||
//
|
||||
// Schema Semantic version: 1.0.0
|
||||
|
||||
// This indicates the flatbuffer compatibility. The number will bump up when a
|
||||
// break change is applied to the schema, such as removing fields or adding new
|
||||
// fields to the middle of a table.
|
||||
file_identifier "V001";
|
||||
|
||||
// History:
|
||||
// 1.0.0 - Initial version.
|
||||
|
||||
// Supported activation functions.
|
||||
enum Activation: byte {
|
||||
NONE = 0,
|
||||
SIGMOID = 1,
|
||||
SOFTMAX = 2
|
||||
}
|
||||
|
||||
table ImageSegmenterOptions {
|
||||
// The activation function of the output layer in the image segmenter.
|
||||
activation: Activation;
|
||||
|
||||
// The minimum necessary image segmenter metadata parser version to fully
|
||||
// understand all fields in a given metadata flatbuffer. This field is
|
||||
// automatically populated by the MetadataPopulator when the metadata is
|
||||
// populated into a TFLite model. This min_parser_version is specific for the
|
||||
// image segmenter metadata defined in this schema file.
|
||||
min_parser_version:string;
|
||||
}
|
||||
|
||||
root_type ImageSegmenterOptions;
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import flatbuffers
|
||||
|
||||
# automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
# namespace: tasks
|
||||
|
||||
from flatbuffers.compat import import_numpy
|
||||
np = import_numpy()
|
||||
|
||||
class Activation(object):
|
||||
NONE = 0
|
||||
SIGMOID = 1
|
||||
SOFTMAX = 2
|
||||
|
||||
|
||||
class ImageSegmenterOptions(object):
|
||||
__slots__ = ['_tab']
|
||||
|
||||
@classmethod
|
||||
def GetRootAs(cls, buf, offset=0):
|
||||
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
|
||||
x = ImageSegmenterOptions()
|
||||
x.Init(buf, n + offset)
|
||||
return x
|
||||
|
||||
@classmethod
|
||||
def GetRootAsImageSegmenterOptions(cls, buf, offset=0):
|
||||
"""This method is deprecated. Please switch to GetRootAs."""
|
||||
return cls.GetRootAs(buf, offset)
|
||||
@classmethod
|
||||
def ImageSegmenterOptionsBufferHasIdentifier(cls, buf, offset, size_prefixed=False):
|
||||
return flatbuffers.util.BufferHasIdentifier(buf, offset, b"\x56\x30\x30\x31", size_prefixed=size_prefixed)
|
||||
|
||||
# ImageSegmenterOptions
|
||||
def Init(self, buf, pos):
|
||||
self._tab = flatbuffers.table.Table(buf, pos)
|
||||
|
||||
# ImageSegmenterOptions
|
||||
def Activation(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
|
||||
if o != 0:
|
||||
return self._tab.Get(flatbuffers.number_types.Int8Flags, o + self._tab.Pos)
|
||||
return 0
|
||||
|
||||
# ImageSegmenterOptions
|
||||
def MinParserVersion(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
|
||||
if o != 0:
|
||||
return self._tab.String(o + self._tab.Pos)
|
||||
return None
|
||||
|
||||
def ImageSegmenterOptionsStart(builder):
|
||||
builder.StartObject(2)
|
||||
|
||||
def ImageSegmenterOptionsAddActivation(builder, activation):
|
||||
builder.PrependInt8Slot(0, activation, 0)
|
||||
|
||||
def ImageSegmenterOptionsAddMinParserVersion(builder, minParserVersion):
|
||||
builder.PrependUOffsetTRelativeSlot(1, flatbuffers.number_types.UOffsetTFlags.py_type(minParserVersion), 0)
|
||||
|
||||
def ImageSegmenterOptionsEnd(builder):
|
||||
return builder.EndObject()
|
||||
|
||||
|
||||
|
||||
class ImageSegmenterOptionsT(object):
|
||||
|
||||
# ImageSegmenterOptionsT
|
||||
def __init__(self):
|
||||
self.activation = 0 # type: int
|
||||
self.minParserVersion = None # type: str
|
||||
|
||||
@classmethod
|
||||
def InitFromBuf(cls, buf, pos):
|
||||
imageSegmenterOptions = ImageSegmenterOptions()
|
||||
imageSegmenterOptions.Init(buf, pos)
|
||||
return cls.InitFromObj(imageSegmenterOptions)
|
||||
|
||||
@classmethod
|
||||
def InitFromPackedBuf(cls, buf, pos=0):
|
||||
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, pos)
|
||||
return cls.InitFromBuf(buf, pos+n)
|
||||
|
||||
@classmethod
|
||||
def InitFromObj(cls, imageSegmenterOptions):
|
||||
x = ImageSegmenterOptionsT()
|
||||
x._UnPack(imageSegmenterOptions)
|
||||
return x
|
||||
|
||||
# ImageSegmenterOptionsT
|
||||
def _UnPack(self, imageSegmenterOptions):
|
||||
if imageSegmenterOptions is None:
|
||||
return
|
||||
self.activation = imageSegmenterOptions.Activation()
|
||||
self.minParserVersion = imageSegmenterOptions.MinParserVersion()
|
||||
|
||||
# ImageSegmenterOptionsT
|
||||
def Pack(self, builder):
|
||||
if self.minParserVersion is not None:
|
||||
minParserVersion = builder.CreateString(self.minParserVersion)
|
||||
ImageSegmenterOptionsStart(builder)
|
||||
ImageSegmenterOptionsAddActivation(builder, self.activation)
|
||||
if self.minParserVersion is not None:
|
||||
ImageSegmenterOptionsAddMinParserVersion(builder, minParserVersion)
|
||||
imageSegmenterOptions = ImageSegmenterOptionsEnd(builder)
|
||||
return imageSegmenterOptions
|
||||
|
||||
|
||||
@@ -0,0 +1,732 @@
|
||||
// Copyright 2022 The MediaPipe Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
namespace tflite;
|
||||
|
||||
// TFLite metadata contains both human readable and machine readable information
|
||||
// about what the model does and how to use the model. It can be used as a
|
||||
// README file, which elaborates the details of the model, each input/ouput
|
||||
// tensor, and each associated file.
|
||||
//
|
||||
// An important use case of TFLite metadata is the TFLite codegen tool, which
|
||||
// automatically generates the model interface based on the properties of the
|
||||
// model and the tensors. The model interface provides high-level APIs to
|
||||
// interact with the model, such as preprocessing the input data and running
|
||||
// inferences.
|
||||
//
|
||||
// Entries marked with "<Codegen usage>" are used in TFLite codegen tool to
|
||||
// generate the model interface. It is recommended to fill in at least those
|
||||
// enties to boost the codegen performance.
|
||||
|
||||
// The Metadata schema is versioned by the Semantic versioning number, such as
|
||||
// MAJOR.MINOR.PATCH. It tracks the schema changes according to the rules below:
|
||||
// * Bump up the MAJOR number when making potentially backwards incompatible
|
||||
// changes. It must be incremented if the new changes break the backwards
|
||||
// compatibility. It may also include minor and patch level changes as
|
||||
// needed. The true backwards compatibility is indicated by the file
|
||||
// identifier.
|
||||
// * Bump up the MINOR number when making backwards compatible updates for
|
||||
// major features, such as supporting new content types or adding new
|
||||
// processing units.
|
||||
// * Bump up the PATCH number when making small backwards compatible changes,
|
||||
// such as adding a new fields or deprecating certain fields (not deleting
|
||||
// them).
|
||||
//
|
||||
// ModelMetadata.min_parser_version indicates the minimum necessary metadata
|
||||
// parser version to fully understand all fields in a given metadata flatbuffer.
|
||||
//
|
||||
// New fields and types will have associated comments with the schema version
|
||||
// for which they were added.
|
||||
//
|
||||
// Schema Semantic version: 1.5.0
|
||||
|
||||
// This indicates the flatbuffer compatibility. The number will bump up when a
|
||||
// break change is applied to the schema, such as removing fields or adding new
|
||||
// fields to the middle of a table.
|
||||
file_identifier "M001";
|
||||
|
||||
// History:
|
||||
// 1.0.1 - Added VOCABULARY type to AssociatedFileType.
|
||||
// 1.1.0 - Added BertTokenizerOptions to ProcessUnitOptions.
|
||||
// Added SentencePieceTokenizerOptions to ProcessUnitOptions.
|
||||
// Added input_process_units to SubGraphMetadata.
|
||||
// Added output_process_units to SubGraphMetadata.
|
||||
// 1.2.0 - Added input_tensor_group to SubGraphMetadata.
|
||||
// Added output_tensor_group to SubGraphMetadata.
|
||||
// 1.2.1 - Added RegexTokenizerOptions to ProcessUnitOptions.
|
||||
// 1.3.0 - Added AudioProperties to ContentProperties.
|
||||
// 1.4.0 - Added SCANN_INDEX_FILE type to AssociatedFileType.
|
||||
// 1.4.1 - Added version to AssociatedFile.
|
||||
// 1.5.0 - Added CustomMetadata in SubGraphMetadata.
|
||||
|
||||
// File extension of any written files.
|
||||
file_extension "tflitemeta";
|
||||
|
||||
enum AssociatedFileType : byte {
|
||||
UNKNOWN = 0,
|
||||
|
||||
// Files such as readme.txt.
|
||||
DESCRIPTIONS = 1,
|
||||
|
||||
// Contains a list of labels (characters separated by "\n" or in lines) that
|
||||
// annotate certain axis of the tensor. For example,
|
||||
// the label file in image classification. Those labels annotate the
|
||||
// the output tensor, such that each value in the output tensor is the
|
||||
// probability of that corresponding category specified by the label. See the
|
||||
// example label file used in image classification [1].
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// If an output tensor has an associated file as TENSOR_AXIS_LABELS, return
|
||||
// the output as a mapping between the labels and probability in the model
|
||||
// interface.
|
||||
// If multiple files of the same type are present, the first one is used by
|
||||
// default; additional ones are to be distinguished from one another by their
|
||||
// specified locale.
|
||||
//
|
||||
// TODO: Add github example link.
|
||||
TENSOR_AXIS_LABELS = 2,
|
||||
|
||||
// Contains a list of labels (characters separated by "\n" or in lines) that
|
||||
// tensor values correspond to. For example, in
|
||||
// the object detection model, one of the output tensors is the detected
|
||||
// classes. And each value in the tensor refers to the index of label in the
|
||||
// category label file. See the example label file used in object detection
|
||||
// [1].
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// If an output tensor has an associated file as TENSOR_VALUE_LABELS, convert
|
||||
// the tensor values into labels, and return a list of string as the output.
|
||||
// If multiple files of the same type are present, the first one is used by
|
||||
// default; additional ones are to be distinguished from one another by their
|
||||
// specified locale.
|
||||
//
|
||||
// TODO: Add github example link.
|
||||
TENSOR_VALUE_LABELS = 3,
|
||||
|
||||
// Contains sigmoid-based score calibration parameters, formatted as CSV.
|
||||
// Lines contain for each index of an output tensor the scale, slope, offset
|
||||
// and (optional) min_score parameters to be used for sigmoid fitting (in this
|
||||
// order and in `strtof`-compatible [1] format). Scale should be a
|
||||
// non-negative value.
|
||||
// A line may be left empty to default calibrated scores for this index to
|
||||
// default_score.
|
||||
// In summary, each line should thus contain 0, 3 or 4 comma-separated values.
|
||||
//
|
||||
// See the example score calibration file used in image classification [2].
|
||||
//
|
||||
// See documentation for ScoreCalibrationOptions for details.
|
||||
//
|
||||
// [1]: https://en.cppreference.com/w/c/string/byte/strtof
|
||||
// TODO: Add github example link.
|
||||
TENSOR_AXIS_SCORE_CALIBRATION = 4,
|
||||
|
||||
// Contains a list of unique words (characters separated by "\n" or in lines)
|
||||
// that help to convert natural language words to embedding vectors.
|
||||
//
|
||||
// See the example vocab file used in text classification [1].
|
||||
//
|
||||
// TODO: Add github example link.
|
||||
// Added in: 1.0.1
|
||||
VOCABULARY = 5,
|
||||
|
||||
// TODO: introduce the ScaNN index file with links once the code
|
||||
// is released.
|
||||
// Contains on-device ScaNN index file with LevelDB format.
|
||||
// Added in: 1.4.0
|
||||
SCANN_INDEX_FILE = 6,
|
||||
}
|
||||
|
||||
table AssociatedFile {
|
||||
// Name of this file. Need to be exact the same as the name of the actual file
|
||||
// packed into the TFLite model as a zip file.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// Locates to the actual file in the TFLite model.
|
||||
name:string;
|
||||
|
||||
// A description of what the file is.
|
||||
description:string;
|
||||
|
||||
// Type of the associated file. There may be special pre/post processing for
|
||||
// some types. For example in image classification, a label file of the output
|
||||
// will be used to convert object index into string.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// Determines how to process the corresponding tensor.
|
||||
type:AssociatedFileType;
|
||||
|
||||
// An optional locale for this associated file (if applicable). It is
|
||||
// recommended to use an ISO 639-1 letter code (e.g. "en" for English),
|
||||
// optionally completed by a two letter region code (e.g. "en-US" for US
|
||||
// English and "en-CA" for Canadian English).
|
||||
// Leverage this in order to specify e.g multiple label files translated in
|
||||
// different languages.
|
||||
locale:string;
|
||||
|
||||
// Version of the file specified by model creators.
|
||||
// Added in: 1.4.1
|
||||
version:string;
|
||||
}
|
||||
|
||||
// The basic content type for all tensors.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// Input feature tensors:
|
||||
// 1. Generates the method to load data from a TensorBuffer.
|
||||
// 2. Creates the preprocessing logic. The default processing pipeline is:
|
||||
// [NormalizeOp, QuantizeOp].
|
||||
// Output feature tensors:
|
||||
// 1. Generates the method to return the output data to a TensorBuffer.
|
||||
// 2. Creates the post-processing logic. The default processing pipeline is:
|
||||
// [DeQuantizeOp].
|
||||
table FeatureProperties {
|
||||
}
|
||||
|
||||
// The type of color space of an image.
|
||||
enum ColorSpaceType : byte {
|
||||
UNKNOWN = 0,
|
||||
RGB = 1,
|
||||
GRAYSCALE = 2,
|
||||
}
|
||||
|
||||
table ImageSize {
|
||||
width:uint;
|
||||
height:uint;
|
||||
}
|
||||
|
||||
// The properties for image tensors.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// Input image tensors:
|
||||
// 1. Generates the method to load an image from a TensorImage.
|
||||
// 2. Creates the preprocessing logic. The default processing pipeline is:
|
||||
// [ResizeOp, NormalizeOp, QuantizeOp].
|
||||
// Output image tensors:
|
||||
// 1. Generates the method to return the output data to a TensorImage.
|
||||
// 2. Creates the post-processing logic. The default processing pipeline is:
|
||||
// [DeQuantizeOp].
|
||||
table ImageProperties {
|
||||
// The color space of the image.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// Determines how to convert the color space of a given image from users.
|
||||
color_space:ColorSpaceType;
|
||||
|
||||
// Indicates the default value of image width and height if the tensor shape
|
||||
// is dynamic. For fixed-size tensor, this size will be consistent with the
|
||||
// expected size.
|
||||
default_size:ImageSize;
|
||||
}
|
||||
|
||||
// The properties for tensors representing bounding boxes.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// Input image tensors: NA.
|
||||
// Output image tensors: parses the values into a data structure that represents
|
||||
// bounding boxes. For example, in the generated wrapper for Android, it returns
|
||||
// the output as android.graphics.Rect objects.
|
||||
enum BoundingBoxType : byte {
|
||||
UNKNOWN = 0,
|
||||
// Represents the bounding box by using the combination of boundaries,
|
||||
// {left, top, right, bottom}.
|
||||
// The default order is {left, top, right, bottom}. Other orders can be
|
||||
// indicated by BoundingBoxProperties.index.
|
||||
BOUNDARIES = 1,
|
||||
|
||||
// Represents the bounding box by using the upper_left corner, width and
|
||||
// height.
|
||||
// The default order is {upper_left_x, upper_left_y, width, height}. Other
|
||||
// orders can be indicated by BoundingBoxProperties.index.
|
||||
UPPER_LEFT = 2,
|
||||
|
||||
// Represents the bounding box by using the center of the box, width and
|
||||
// height. The default order is {center_x, center_y, width, height}. Other
|
||||
// orders can be indicated by BoundingBoxProperties.index.
|
||||
CENTER = 3,
|
||||
|
||||
}
|
||||
|
||||
// The properties for audio tensors.
|
||||
// Added in: 1.3.0
|
||||
table AudioProperties {
|
||||
// The sample rate in Hz when the audio was captured.
|
||||
sample_rate:uint;
|
||||
|
||||
// The channel count of the audio.
|
||||
channels:uint;
|
||||
}
|
||||
|
||||
enum CoordinateType : byte {
|
||||
// The coordinates are float values from 0 to 1.
|
||||
RATIO = 0,
|
||||
// The coordinates are integers.
|
||||
PIXEL = 1,
|
||||
}
|
||||
|
||||
table BoundingBoxProperties {
|
||||
// Denotes the order of the elements defined in each bounding box type. An
|
||||
// empty index array represent the default order of each bounding box type.
|
||||
// For example, to denote the default order of BOUNDARIES, {left, top, right,
|
||||
// bottom}, the index should be {0, 1, 2, 3}. To denote the order {left,
|
||||
// right, top, bottom}, the order should be {0, 2, 1, 3}.
|
||||
//
|
||||
// The index array can be applied to all bounding box types to adjust the
|
||||
// order of their corresponding underlying elements.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// Indicates how to parse the bounding box values.
|
||||
index:[uint];
|
||||
|
||||
// <Codegen usage>:
|
||||
// Indicates how to parse the bounding box values.
|
||||
type:BoundingBoxType;
|
||||
|
||||
// <Codegen usage>:
|
||||
// Indicates how to convert the bounding box back to the original image in
|
||||
// pixels.
|
||||
coordinate_type:CoordinateType;
|
||||
}
|
||||
|
||||
union ContentProperties {
|
||||
FeatureProperties,
|
||||
ImageProperties,
|
||||
BoundingBoxProperties,
|
||||
// Added in: 1.3.0
|
||||
AudioProperties,
|
||||
}
|
||||
|
||||
table ValueRange {
|
||||
min:int;
|
||||
max:int;
|
||||
}
|
||||
|
||||
table Content {
|
||||
// The properties that the content may have, indicating the type of the
|
||||
// Content.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// Indicates how to process the tensor.
|
||||
content_properties:ContentProperties;
|
||||
|
||||
// The range of dimensions that the content corresponds to. A NULL
|
||||
// "range" indicates that the content uses up all dimensions,
|
||||
// except the batch axis if applied.
|
||||
//
|
||||
// Here are all the possible situations of how a tensor is composed.
|
||||
// Case 1: The tensor is a single object, such as an image.
|
||||
// For example, the input of an image classifier
|
||||
// (https://www.tensorflow.org/lite/models/image_classification/overview),
|
||||
// a tensor of shape [1, 224, 224, 3]. Dimensions 1 to 3 correspond to the
|
||||
// image. Since dimension 0 is a batch axis, which can be ignored,
|
||||
// "range" can be left as NULL.
|
||||
//
|
||||
// Case 2: The tensor contains multiple instances of the same object.
|
||||
// For example, the output tensor of detected bounding boxes of an object
|
||||
// detection model
|
||||
// (https://www.tensorflow.org/lite/models/object_detection/overview).
|
||||
// The tensor shape is [1, 10, 4]. Here is the what the three dimensions
|
||||
// represent for:
|
||||
// dimension 0: the batch axis.
|
||||
// dimension 1: the 10 objects detected with the highest confidence.
|
||||
// dimension 2: the bounding boxes of the 10 detected objects.
|
||||
// The tensor is essentially 10 bounding boxes. In this case,
|
||||
// "range" should be {min=2; max=2;}.
|
||||
//
|
||||
// The output tensor of scores of the above object detection model has shape
|
||||
// [1, 10], where
|
||||
// dimension 0: the batch axis;
|
||||
// dimension 1: the scores of the 10 detected objects.
|
||||
// Set "range" to the number of dimensions which is {min=2; max=2;} to denote
|
||||
// that every element in the tensor is an individual content object, i.e. a
|
||||
// score in this example.
|
||||
//
|
||||
// Another example is the pose estimation model
|
||||
// (https://www.tensorflow.org/lite/models/pose_estimation/overview).
|
||||
// The output tensor of heatmaps is in the shape of [1, 9, 9, 17].
|
||||
// Here is the what the four dimensions represent for:
|
||||
// dimension 0: the batch axis.
|
||||
// dimension 1/2: the heatmap image.
|
||||
// dimension 3: 17 body parts of a person.
|
||||
// Even though the last axis is body part, the real content of this tensor is
|
||||
// the heatmap. "range" should be [min=1; max=2].
|
||||
//
|
||||
// Case 3: The tensor contains multiple different objects. (Not supported by
|
||||
// Content at this point).
|
||||
// Sometimes a tensor may contain multiple different objects, thus different
|
||||
// contents. It is very common for regression models. For example, a model
|
||||
// to predict the fuel efficiency
|
||||
// (https://www.tensorflow.org/tutorials/keras/regression).
|
||||
// The input tensor has shape [1, 9], consisting of 9 features, such as
|
||||
// "Cylinders", "Displacement", "Weight", etc. In this case, dimension 1
|
||||
// contains 9 different contents. However, since these sub-dimension objects
|
||||
// barely need to be specifically processed, their contents are not recorded
|
||||
// in the metadata. Through, the name of each dimension can be set through
|
||||
// TensorMetadata.dimension_names.
|
||||
//
|
||||
// Note that if it is not case 3, a tensor can only have one content type.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// Case 1: return a processed single object of certain content type.
|
||||
// Case 2: return a list of processed objects of certain content type. The
|
||||
// generated model interface have API to random access those objects from
|
||||
// the output.
|
||||
range:ValueRange;
|
||||
}
|
||||
|
||||
// Parameters that are used when normalizing the tensor.
|
||||
table NormalizationOptions{
|
||||
// mean and std are normalization parameters. Tensor values are normalized
|
||||
// on a per-channel basis, by the formula
|
||||
// (x - mean) / std.
|
||||
// If there is only one value in mean or std, we'll propagate the value to
|
||||
// all channels.
|
||||
//
|
||||
// Quantized models share the same normalization parameters as their
|
||||
// corresponding float models. For example, an image input tensor may have
|
||||
// the normalization parameter of
|
||||
// mean = 127.5f and std = 127.5f.
|
||||
// The image value will be normalized from [0, 255] to [-1, 1].
|
||||
// Then, for quantized models, the image data should be further quantized
|
||||
// according to the quantization parameters. In the case of uint8, the image
|
||||
// data will be scaled back to [0, 255], while for int8, the image data will
|
||||
// be scaled to [-128, 127].
|
||||
//
|
||||
// Both the normalization parameters and quantization parameters can be
|
||||
// retrieved through the metadata extractor library.
|
||||
// TODO: add link for the metadata extractor library.
|
||||
|
||||
// Per-channel mean of the possible values used in normalization.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// Apply normalization to input tensors accordingly.
|
||||
mean:[float];
|
||||
|
||||
// Per-channel standard dev. of the possible values used in normalization.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// Apply normalization to input tensors accordingly.
|
||||
std:[float];
|
||||
}
|
||||
|
||||
// The different possible score transforms to apply to uncalibrated scores
|
||||
// before applying score calibration.
|
||||
enum ScoreTransformationType : byte {
|
||||
// Identity function: g(x) = x.
|
||||
IDENTITY = 0,
|
||||
// Log function: g(x) = log(x).
|
||||
LOG = 1,
|
||||
// Inverse logistic function: g(x) = log(x) - log(1-x).
|
||||
INVERSE_LOGISTIC = 2,
|
||||
}
|
||||
|
||||
// Options to perform score calibration on an output tensor through sigmoid
|
||||
// functions. One of the main purposes of score calibration is to make scores
|
||||
// across classes comparable, so that a common threshold can be used for all
|
||||
// output classes. This is meant for models producing class predictions as
|
||||
// output, e.g. image classification or detection models.
|
||||
//
|
||||
// For each index in the output tensor, this applies:
|
||||
// * `f(x) = scale / (1 + e^-(slope*g(x)+offset))` if `x > min_score` or if no
|
||||
// `min_score` has been specified,
|
||||
// * `f(x) = default_score` otherwise or if no scale, slope and offset have been
|
||||
// specified.
|
||||
// Where:
|
||||
// * scale, slope, offset and (optional) min_score are index-specific parameters
|
||||
// * g(x) is an index-independent transform among those defined in
|
||||
// ScoreTransformationType
|
||||
// * default_score is an index-independent parameter.
|
||||
// An AssociatedFile with type TANSOR_AXIS_SCORE_CALIBRATION specifying the
|
||||
// index-specific parameters must be associated with the corresponding
|
||||
// TensorMetadata for score calibration be applied.
|
||||
//
|
||||
// See the example score calibration file used in image classification [1].
|
||||
// TODO: Add github example link.
|
||||
table ScoreCalibrationOptions {
|
||||
// The function to use for transforming the uncalibrated score before
|
||||
// applying score calibration.
|
||||
score_transformation:ScoreTransformationType;
|
||||
|
||||
// The default calibrated score to apply if the uncalibrated score is
|
||||
// below min_score or if no parameters were specified for a given index.
|
||||
default_score:float;
|
||||
}
|
||||
|
||||
// Performs thresholding on output tensor values, in order to filter out
|
||||
// low-confidence results.
|
||||
table ScoreThresholdingOptions {
|
||||
// The recommended global threshold below which results are considered
|
||||
// low-confidence and should be filtered out.
|
||||
global_score_threshold:float;
|
||||
}
|
||||
|
||||
// Performs Bert tokenization as in tf.text.BertTokenizer
|
||||
// (https://github.com/tensorflow/text/blob/3599f6fcd2b780a2dc413b90fb9315464f10b314/docs/api_docs/python/text/BertTokenizer.md)
|
||||
// Added in: 1.1.0
|
||||
table BertTokenizerOptions {
|
||||
// The vocabulary files used in the BertTokenizer.
|
||||
vocab_file:[AssociatedFile];
|
||||
}
|
||||
|
||||
// Performs SentencePiece tokenization as in tf.text.SentencepieceTokenizer
|
||||
// (https://github.com/tensorflow/text/blob/3599f6fcd2b780a2dc413b90fb9315464f10b314/docs/api_docs/python/text/SentencepieceTokenizer.md).
|
||||
// Added in: 1.1.0
|
||||
table SentencePieceTokenizerOptions {
|
||||
// The SentencePiece model files used in the SentencePieceTokenizer.
|
||||
sentencePiece_model:[AssociatedFile];
|
||||
|
||||
// The optional vocabulary model files used in the SentencePieceTokenizer.
|
||||
vocab_file:[AssociatedFile];
|
||||
}
|
||||
|
||||
// Splits strings by the occurrences of delim_regex_pattern and converts the
|
||||
// tokens into ids. For example, given
|
||||
// delim_regex_pattern: "\W+",
|
||||
// string: "Words, words, words.",
|
||||
// the tokens after split are: "Words", "words", "words", "".
|
||||
// And then the tokens can be converted into ids according to the vocab_file.
|
||||
// Added in: 1.2.1
|
||||
table RegexTokenizerOptions {
|
||||
delim_regex_pattern:string;
|
||||
// The vocabulary files used to convert this tokens into ids.
|
||||
vocab_file:[AssociatedFile];
|
||||
}
|
||||
|
||||
// Options that are used when processing the tensor.
|
||||
union ProcessUnitOptions {
|
||||
NormalizationOptions,
|
||||
ScoreCalibrationOptions,
|
||||
ScoreThresholdingOptions,
|
||||
// Added in: 1.1.0
|
||||
BertTokenizerOptions,
|
||||
// Added in: 1.1.0
|
||||
SentencePieceTokenizerOptions,
|
||||
// Added in: 1.2.1
|
||||
RegexTokenizerOptions
|
||||
}
|
||||
|
||||
// A process unit that is used to process the tensor out-of-graph.
|
||||
table ProcessUnit {
|
||||
options:ProcessUnitOptions;
|
||||
}
|
||||
|
||||
|
||||
// Statistics to describe a tensor.
|
||||
table Stats {
|
||||
// Max and min are not currently used in tflite.support codegen. They mainly
|
||||
// serve as references for users to better understand the model. They can also
|
||||
// be used to validate model pre/post processing results.
|
||||
// If there is only one value in max or min, we'll propagate the value to
|
||||
// all channels.
|
||||
|
||||
// Per-channel maximum value of the tensor.
|
||||
max:[float];
|
||||
|
||||
// Per-channel minimum value of the tensor.
|
||||
min:[float];
|
||||
}
|
||||
|
||||
// Metadata of a group of tensors. It may contain several tensors that will be
|
||||
// grouped together in codegen. For example, the TFLite object detection model
|
||||
// example (https://www.tensorflow.org/lite/models/object_detection/overview)
|
||||
// has four outputs: classes, scores, bounding boxes, and number of detections.
|
||||
// If the four outputs are bundled together using TensorGroup (for example,
|
||||
// named as "detection result"), the codegen tool will generate the class,
|
||||
// `DetectionResult`, which contains the class, score, and bounding box. And the
|
||||
// outputs of the model will be converted to a list of `DetectionResults` and
|
||||
// the number of detection. Note that the number of detection is a single
|
||||
// number, therefore is inappropriate for the list of `DetectionResult`.
|
||||
// Added in: 1.2.0
|
||||
table TensorGroup {
|
||||
// Name of tensor group.
|
||||
//
|
||||
// <codegen usage>:
|
||||
// Name of the joint class of the tensor group.
|
||||
name:string;
|
||||
|
||||
// Names of the tensors to group together, corresponding to
|
||||
// TensorMetadata.name.
|
||||
//
|
||||
// <codegen usage>:
|
||||
// Determines which tensors will be added to this group. All tensors in the
|
||||
// group should have the same number of elements specified by Content.range.
|
||||
tensor_names:[string];
|
||||
}
|
||||
|
||||
// Detailed information of an input or output tensor.
|
||||
table TensorMetadata {
|
||||
// Name of the tensor.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// The name of this tensor in the generated model interface.
|
||||
name:string;
|
||||
|
||||
// A description of the tensor.
|
||||
description:string;
|
||||
|
||||
// A list of names of the dimensions in this tensor. The length of
|
||||
// dimension_names need to match the number of dimensions in this tensor.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// The name of each dimension in the generated model interface. See "Case 2"
|
||||
// in the comments of Content.range.
|
||||
dimension_names:[string];
|
||||
|
||||
// The content that represents this tensor.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// Determines how to process this tensor. See each item in ContentProperties
|
||||
// for the default process units that will be applied to the tensor.
|
||||
content:Content;
|
||||
|
||||
// The process units that are used to process the tensor out-of-graph.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// Contains the parameters of the default processing pipeline for each content
|
||||
// type, such as the normalization parameters in all content types. See the
|
||||
// items under ContentProperties for the details of the default processing
|
||||
// pipeline.
|
||||
process_units:[ProcessUnit];
|
||||
|
||||
// The statistics of the tensor values.
|
||||
stats:Stats;
|
||||
|
||||
// A list of associated files of this tensor.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// Contains processing parameters of this tensor, such as normalization.
|
||||
associated_files:[AssociatedFile];
|
||||
}
|
||||
|
||||
table CustomMetadata {
|
||||
name:string;
|
||||
data:[ubyte] (force_align: 16);
|
||||
}
|
||||
|
||||
table SubGraphMetadata {
|
||||
// Name of the subgraph.
|
||||
//
|
||||
// Note that, since TFLite only support one subgraph at this moment, the
|
||||
// Codegen tool will use the name in ModelMetadata in the generated model
|
||||
// interface.
|
||||
name:string;
|
||||
|
||||
// A description explains details about what the subgraph does.
|
||||
description:string;
|
||||
|
||||
// Metadata of all input tensors used in this subgraph. It matches exactly
|
||||
// the input tensors specified by `SubGraph.inputs` in the TFLite
|
||||
// schema.fbs file[2]. The number of `TensorMetadata` in the array should
|
||||
// equal to the number of indices in `SubGraph.inputs`.
|
||||
//
|
||||
// [2]: tensorflow/lite/schema/schema.fbs
|
||||
// <Codegen usage>:
|
||||
// Determines how to process the inputs.
|
||||
input_tensor_metadata:[TensorMetadata];
|
||||
|
||||
// Metadata of all output tensors used in this subgraph. It matches exactly
|
||||
// the output tensors specified by `SubGraph.outputs` in the TFLite
|
||||
// schema.fbs file[2]. The number of `TensorMetadata` in the array should
|
||||
// equal to the number of indices in `SubGraph.outputs`.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// Determines how to process the outputs.
|
||||
output_tensor_metadata:[TensorMetadata];
|
||||
|
||||
// A list of associated files of this subgraph.
|
||||
associated_files:[AssociatedFile];
|
||||
|
||||
// Input process units of the subgraph. Some models may have complex pre and
|
||||
// post processing logics where the process units do not work on one tensor at
|
||||
// a time, but in a similar way of a TFLite graph. For example, in the
|
||||
// MobileBert model (https://www.tensorflow.org/lite/models/bert_qa/overview),
|
||||
// the inputs are: ids / mask / segment ids;
|
||||
// the outputs are: end logits / start logits.
|
||||
// The preprocessing converts the query string and the context string to the
|
||||
// model inputs, and the post-processing converts the model outputs to the
|
||||
// answer string.
|
||||
// Added in: 1.1.0
|
||||
input_process_units:[ProcessUnit];
|
||||
|
||||
// Output process units of the subgraph.
|
||||
// Added in: 1.1.0
|
||||
output_process_units:[ProcessUnit];
|
||||
|
||||
// Metadata of all input tensor groups used in this subgraph.
|
||||
//
|
||||
// <codegen usage>:
|
||||
// Bundles the corresponding elements of the underlying input tensors together
|
||||
// into a class, and converts those individual tensors into a list of the
|
||||
// class objects.
|
||||
// Added in: 1.2.0
|
||||
input_tensor_groups:[TensorGroup];
|
||||
|
||||
// Metadata of all output tensor groups used in this subgraph.
|
||||
//
|
||||
// <codegen usage>:
|
||||
// Bundles the corresponding elements of the underlying output tensors
|
||||
// together into a class, and converts those individual tensors into a list of
|
||||
// the class objects.
|
||||
// Added in: 1.2.0
|
||||
output_tensor_groups:[TensorGroup];
|
||||
|
||||
// A list of custom metadata.
|
||||
// Added in: 1.5.0.
|
||||
custom_metadata:[CustomMetadata];
|
||||
}
|
||||
|
||||
table ModelMetadata {
|
||||
// Name of the model.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// The name of the model in the generated model interface.
|
||||
name:string;
|
||||
|
||||
// Model description in schema.
|
||||
description:string;
|
||||
|
||||
// Version of the model that specified by model creators.
|
||||
version:string;
|
||||
|
||||
// Noted that, the minimum required TFLite runtime version that the model is
|
||||
// compatible with, has already been added as a metadata entry in tflite
|
||||
// schema. We'll decide later if we want to move it here, and keep it with
|
||||
// other metadata entries.
|
||||
|
||||
// Metadata of all the subgraphs of the model. The 0th is assumed to be the
|
||||
// main subgraph.
|
||||
//
|
||||
// <Codegen usage>:
|
||||
// Determines how to process the inputs and outputs.
|
||||
subgraph_metadata:[SubGraphMetadata];
|
||||
|
||||
// The person who creates this model.
|
||||
author:string;
|
||||
|
||||
// Licenses that may apply to this model.
|
||||
license:string;
|
||||
|
||||
// A list of associated files of this model.
|
||||
associated_files:[AssociatedFile];
|
||||
|
||||
// The minimum metadata parser version that can fully understand the fields in
|
||||
// the metadata flatbuffer. The version is effectively the largest version
|
||||
// number among the versions of all the fields populated and the smallest
|
||||
// compatible version indicated by the file identifier.
|
||||
//
|
||||
// This field is automatically populated by the MetadataPopulator when
|
||||
// the metadata is populated into a TFLite model.
|
||||
min_parser_version:string;
|
||||
}
|
||||
|
||||
root_type ModelMetadata;
|
||||
+3251
File diff suppressed because it is too large
Load Diff
+98
@@ -0,0 +1,98 @@
|
||||
// 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.
|
||||
|
||||
namespace mediapipe.tasks;
|
||||
|
||||
// ObjectDetectorOptions.min_parser_version indicates the minimum necessary
|
||||
// object detector metadata parser version to fully understand all fields in a
|
||||
// given metadata flatbuffer. This min_parser_version is specific for the
|
||||
// object detector metadata defined in this schema file.
|
||||
//
|
||||
// New fields and types will have associated comments with the schema version
|
||||
// for which they were added.
|
||||
//
|
||||
// Schema Semantic version: 1.0.0
|
||||
|
||||
// This indicates the flatbuffer compatibility. The number will bump up when a
|
||||
// break change is applied to the schema, such as removing fields or adding new
|
||||
// fields to the middle of a table.
|
||||
file_identifier "V001";
|
||||
|
||||
|
||||
// History:
|
||||
// 1.0.0 - Initial version.
|
||||
|
||||
// A fixed size anchor.
|
||||
table FixedAnchor {
|
||||
x_center: float;
|
||||
y_center: float;
|
||||
width: float;
|
||||
height: float;
|
||||
}
|
||||
|
||||
// The schema for a list of anchors with fixed size.
|
||||
table FixedAnchorsSchema {
|
||||
anchors: [FixedAnchor];
|
||||
}
|
||||
|
||||
// The ssd anchors options used in the object detector.
|
||||
table SsdAnchorsOptions {
|
||||
fixed_anchors_schema: FixedAnchorsSchema;
|
||||
}
|
||||
|
||||
// The options for decoding the raw model output tensors. The options are mostly
|
||||
// used in TensorsToDetectionsCalculatorOptions.
|
||||
table TensorsDecodingOptions {
|
||||
// The number of output classes predicted by the detection model.
|
||||
num_classes: int;
|
||||
// The number of output boxes predicted by the detection model.
|
||||
num_boxes: int;
|
||||
// The number of output values per boxes predicted by the detection
|
||||
// model. The values contain bounding boxes, keypoints, etc.
|
||||
num_coords: int;
|
||||
// The offset of keypoint coordinates in the location tensor.
|
||||
keypoint_coord_offset: int;
|
||||
// The number of predicted keypoints.
|
||||
num_keypoints: int;
|
||||
// The dimension of each keypoint, e.g. number of values predicted for each
|
||||
// keypoint.
|
||||
num_values_per_keypoint: int;
|
||||
// Parameters for decoding SSD detection model.
|
||||
x_scale: float;
|
||||
y_scale: float;
|
||||
w_scale: float;
|
||||
h_scale: float;
|
||||
// Whether to apply exponential on box size.
|
||||
apply_exponential_on_box_size: bool;
|
||||
// Whether to apply sigmod function on the score.
|
||||
sigmoid_score: bool;
|
||||
}
|
||||
|
||||
table ObjectDetectorOptions {
|
||||
// TODO: automatically populate min parser string.
|
||||
// The minimum necessary object detector metadata parser version to fully
|
||||
// understand all fields in a given metadata flatbuffer. This field is
|
||||
// automatically populated by the MetadataPopulator when the metadata is
|
||||
// populated into a TFLite model. This min_parser_version is specific for the
|
||||
// object detector metadata defined in this schema file.
|
||||
min_parser_version:string;
|
||||
|
||||
// The options of ssd anchors configs used by the detection model.
|
||||
ssd_anchors_options:SsdAnchorsOptions;
|
||||
|
||||
// The tensors decoding options to convert raw tensors to detection results.
|
||||
tensors_decoding_options:TensorsDecodingOptions;
|
||||
}
|
||||
|
||||
root_type ObjectDetectorOptions;
|
||||
+674
@@ -0,0 +1,674 @@
|
||||
import flatbuffers
|
||||
|
||||
# automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
# namespace: tasks
|
||||
|
||||
from flatbuffers.compat import import_numpy
|
||||
np = import_numpy()
|
||||
|
||||
class FixedAnchor(object):
|
||||
__slots__ = ['_tab']
|
||||
|
||||
@classmethod
|
||||
def GetRootAs(cls, buf, offset=0):
|
||||
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
|
||||
x = FixedAnchor()
|
||||
x.Init(buf, n + offset)
|
||||
return x
|
||||
|
||||
@classmethod
|
||||
def GetRootAsFixedAnchor(cls, buf, offset=0):
|
||||
"""This method is deprecated. Please switch to GetRootAs."""
|
||||
return cls.GetRootAs(buf, offset)
|
||||
@classmethod
|
||||
def FixedAnchorBufferHasIdentifier(cls, buf, offset, size_prefixed=False):
|
||||
return flatbuffers.util.BufferHasIdentifier(buf, offset, b"\x56\x30\x30\x31", size_prefixed=size_prefixed)
|
||||
|
||||
# FixedAnchor
|
||||
def Init(self, buf, pos):
|
||||
self._tab = flatbuffers.table.Table(buf, pos)
|
||||
|
||||
# FixedAnchor
|
||||
def XCenter(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
|
||||
if o != 0:
|
||||
return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos)
|
||||
return 0.0
|
||||
|
||||
# FixedAnchor
|
||||
def YCenter(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
|
||||
if o != 0:
|
||||
return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos)
|
||||
return 0.0
|
||||
|
||||
# FixedAnchor
|
||||
def Width(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
|
||||
if o != 0:
|
||||
return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos)
|
||||
return 0.0
|
||||
|
||||
# FixedAnchor
|
||||
def Height(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
|
||||
if o != 0:
|
||||
return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos)
|
||||
return 0.0
|
||||
|
||||
def FixedAnchorStart(builder):
|
||||
builder.StartObject(4)
|
||||
|
||||
def FixedAnchorAddXCenter(builder, xCenter):
|
||||
builder.PrependFloat32Slot(0, xCenter, 0.0)
|
||||
|
||||
def FixedAnchorAddYCenter(builder, yCenter):
|
||||
builder.PrependFloat32Slot(1, yCenter, 0.0)
|
||||
|
||||
def FixedAnchorAddWidth(builder, width):
|
||||
builder.PrependFloat32Slot(2, width, 0.0)
|
||||
|
||||
def FixedAnchorAddHeight(builder, height):
|
||||
builder.PrependFloat32Slot(3, height, 0.0)
|
||||
|
||||
def FixedAnchorEnd(builder):
|
||||
return builder.EndObject()
|
||||
|
||||
|
||||
|
||||
class FixedAnchorT(object):
|
||||
|
||||
# FixedAnchorT
|
||||
def __init__(self):
|
||||
self.xCenter = 0.0 # type: float
|
||||
self.yCenter = 0.0 # type: float
|
||||
self.width = 0.0 # type: float
|
||||
self.height = 0.0 # type: float
|
||||
|
||||
@classmethod
|
||||
def InitFromBuf(cls, buf, pos):
|
||||
fixedAnchor = FixedAnchor()
|
||||
fixedAnchor.Init(buf, pos)
|
||||
return cls.InitFromObj(fixedAnchor)
|
||||
|
||||
@classmethod
|
||||
def InitFromPackedBuf(cls, buf, pos=0):
|
||||
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, pos)
|
||||
return cls.InitFromBuf(buf, pos+n)
|
||||
|
||||
@classmethod
|
||||
def InitFromObj(cls, fixedAnchor):
|
||||
x = FixedAnchorT()
|
||||
x._UnPack(fixedAnchor)
|
||||
return x
|
||||
|
||||
# FixedAnchorT
|
||||
def _UnPack(self, fixedAnchor):
|
||||
if fixedAnchor is None:
|
||||
return
|
||||
self.xCenter = fixedAnchor.XCenter()
|
||||
self.yCenter = fixedAnchor.YCenter()
|
||||
self.width = fixedAnchor.Width()
|
||||
self.height = fixedAnchor.Height()
|
||||
|
||||
# FixedAnchorT
|
||||
def Pack(self, builder):
|
||||
FixedAnchorStart(builder)
|
||||
FixedAnchorAddXCenter(builder, self.xCenter)
|
||||
FixedAnchorAddYCenter(builder, self.yCenter)
|
||||
FixedAnchorAddWidth(builder, self.width)
|
||||
FixedAnchorAddHeight(builder, self.height)
|
||||
fixedAnchor = FixedAnchorEnd(builder)
|
||||
return fixedAnchor
|
||||
|
||||
|
||||
class FixedAnchorsSchema(object):
|
||||
__slots__ = ['_tab']
|
||||
|
||||
@classmethod
|
||||
def GetRootAs(cls, buf, offset=0):
|
||||
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
|
||||
x = FixedAnchorsSchema()
|
||||
x.Init(buf, n + offset)
|
||||
return x
|
||||
|
||||
@classmethod
|
||||
def GetRootAsFixedAnchorsSchema(cls, buf, offset=0):
|
||||
"""This method is deprecated. Please switch to GetRootAs."""
|
||||
return cls.GetRootAs(buf, offset)
|
||||
@classmethod
|
||||
def FixedAnchorsSchemaBufferHasIdentifier(cls, buf, offset, size_prefixed=False):
|
||||
return flatbuffers.util.BufferHasIdentifier(buf, offset, b"\x56\x30\x30\x31", size_prefixed=size_prefixed)
|
||||
|
||||
# FixedAnchorsSchema
|
||||
def Init(self, buf, pos):
|
||||
self._tab = flatbuffers.table.Table(buf, pos)
|
||||
|
||||
# FixedAnchorsSchema
|
||||
def Anchors(self, j):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
|
||||
if o != 0:
|
||||
x = self._tab.Vector(o)
|
||||
x += flatbuffers.number_types.UOffsetTFlags.py_type(j) * 4
|
||||
x = self._tab.Indirect(x)
|
||||
obj = FixedAnchor()
|
||||
obj.Init(self._tab.Bytes, x)
|
||||
return obj
|
||||
return None
|
||||
|
||||
# FixedAnchorsSchema
|
||||
def AnchorsLength(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
|
||||
if o != 0:
|
||||
return self._tab.VectorLen(o)
|
||||
return 0
|
||||
|
||||
# FixedAnchorsSchema
|
||||
def AnchorsIsNone(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
|
||||
return o == 0
|
||||
|
||||
def FixedAnchorsSchemaStart(builder):
|
||||
builder.StartObject(1)
|
||||
|
||||
def FixedAnchorsSchemaAddAnchors(builder, anchors):
|
||||
builder.PrependUOffsetTRelativeSlot(0, flatbuffers.number_types.UOffsetTFlags.py_type(anchors), 0)
|
||||
|
||||
def FixedAnchorsSchemaStartAnchorsVector(builder, numElems):
|
||||
return builder.StartVector(4, numElems, 4)
|
||||
|
||||
def FixedAnchorsSchemaEnd(builder):
|
||||
return builder.EndObject()
|
||||
|
||||
|
||||
try:
|
||||
from typing import List
|
||||
except:
|
||||
pass
|
||||
|
||||
class FixedAnchorsSchemaT(object):
|
||||
|
||||
# FixedAnchorsSchemaT
|
||||
def __init__(self):
|
||||
self.anchors = None # type: List[FixedAnchorT]
|
||||
|
||||
@classmethod
|
||||
def InitFromBuf(cls, buf, pos):
|
||||
fixedAnchorsSchema = FixedAnchorsSchema()
|
||||
fixedAnchorsSchema.Init(buf, pos)
|
||||
return cls.InitFromObj(fixedAnchorsSchema)
|
||||
|
||||
@classmethod
|
||||
def InitFromPackedBuf(cls, buf, pos=0):
|
||||
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, pos)
|
||||
return cls.InitFromBuf(buf, pos+n)
|
||||
|
||||
@classmethod
|
||||
def InitFromObj(cls, fixedAnchorsSchema):
|
||||
x = FixedAnchorsSchemaT()
|
||||
x._UnPack(fixedAnchorsSchema)
|
||||
return x
|
||||
|
||||
# FixedAnchorsSchemaT
|
||||
def _UnPack(self, fixedAnchorsSchema):
|
||||
if fixedAnchorsSchema is None:
|
||||
return
|
||||
if not fixedAnchorsSchema.AnchorsIsNone():
|
||||
self.anchors = []
|
||||
for i in range(fixedAnchorsSchema.AnchorsLength()):
|
||||
if fixedAnchorsSchema.Anchors(i) is None:
|
||||
self.anchors.append(None)
|
||||
else:
|
||||
fixedAnchor_ = FixedAnchorT.InitFromObj(fixedAnchorsSchema.Anchors(i))
|
||||
self.anchors.append(fixedAnchor_)
|
||||
|
||||
# FixedAnchorsSchemaT
|
||||
def Pack(self, builder):
|
||||
if self.anchors is not None:
|
||||
anchorslist = []
|
||||
for i in range(len(self.anchors)):
|
||||
anchorslist.append(self.anchors[i].Pack(builder))
|
||||
FixedAnchorsSchemaStartAnchorsVector(builder, len(self.anchors))
|
||||
for i in reversed(range(len(self.anchors))):
|
||||
builder.PrependUOffsetTRelative(anchorslist[i])
|
||||
anchors = builder.EndVector()
|
||||
FixedAnchorsSchemaStart(builder)
|
||||
if self.anchors is not None:
|
||||
FixedAnchorsSchemaAddAnchors(builder, anchors)
|
||||
fixedAnchorsSchema = FixedAnchorsSchemaEnd(builder)
|
||||
return fixedAnchorsSchema
|
||||
|
||||
|
||||
class SsdAnchorsOptions(object):
|
||||
__slots__ = ['_tab']
|
||||
|
||||
@classmethod
|
||||
def GetRootAs(cls, buf, offset=0):
|
||||
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
|
||||
x = SsdAnchorsOptions()
|
||||
x.Init(buf, n + offset)
|
||||
return x
|
||||
|
||||
@classmethod
|
||||
def GetRootAsSsdAnchorsOptions(cls, buf, offset=0):
|
||||
"""This method is deprecated. Please switch to GetRootAs."""
|
||||
return cls.GetRootAs(buf, offset)
|
||||
@classmethod
|
||||
def SsdAnchorsOptionsBufferHasIdentifier(cls, buf, offset, size_prefixed=False):
|
||||
return flatbuffers.util.BufferHasIdentifier(buf, offset, b"\x56\x30\x30\x31", size_prefixed=size_prefixed)
|
||||
|
||||
# SsdAnchorsOptions
|
||||
def Init(self, buf, pos):
|
||||
self._tab = flatbuffers.table.Table(buf, pos)
|
||||
|
||||
# SsdAnchorsOptions
|
||||
def FixedAnchorsSchema(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
|
||||
if o != 0:
|
||||
x = self._tab.Indirect(o + self._tab.Pos)
|
||||
obj = FixedAnchorsSchema()
|
||||
obj.Init(self._tab.Bytes, x)
|
||||
return obj
|
||||
return None
|
||||
|
||||
def SsdAnchorsOptionsStart(builder):
|
||||
builder.StartObject(1)
|
||||
|
||||
def SsdAnchorsOptionsAddFixedAnchorsSchema(builder, fixedAnchorsSchema):
|
||||
builder.PrependUOffsetTRelativeSlot(0, flatbuffers.number_types.UOffsetTFlags.py_type(fixedAnchorsSchema), 0)
|
||||
|
||||
def SsdAnchorsOptionsEnd(builder):
|
||||
return builder.EndObject()
|
||||
|
||||
|
||||
try:
|
||||
from typing import Optional
|
||||
except:
|
||||
pass
|
||||
|
||||
class SsdAnchorsOptionsT(object):
|
||||
|
||||
# SsdAnchorsOptionsT
|
||||
def __init__(self):
|
||||
self.fixedAnchorsSchema = None # type: Optional[FixedAnchorsSchemaT]
|
||||
|
||||
@classmethod
|
||||
def InitFromBuf(cls, buf, pos):
|
||||
ssdAnchorsOptions = SsdAnchorsOptions()
|
||||
ssdAnchorsOptions.Init(buf, pos)
|
||||
return cls.InitFromObj(ssdAnchorsOptions)
|
||||
|
||||
@classmethod
|
||||
def InitFromPackedBuf(cls, buf, pos=0):
|
||||
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, pos)
|
||||
return cls.InitFromBuf(buf, pos+n)
|
||||
|
||||
@classmethod
|
||||
def InitFromObj(cls, ssdAnchorsOptions):
|
||||
x = SsdAnchorsOptionsT()
|
||||
x._UnPack(ssdAnchorsOptions)
|
||||
return x
|
||||
|
||||
# SsdAnchorsOptionsT
|
||||
def _UnPack(self, ssdAnchorsOptions):
|
||||
if ssdAnchorsOptions is None:
|
||||
return
|
||||
if ssdAnchorsOptions.FixedAnchorsSchema() is not None:
|
||||
self.fixedAnchorsSchema = FixedAnchorsSchemaT.InitFromObj(ssdAnchorsOptions.FixedAnchorsSchema())
|
||||
|
||||
# SsdAnchorsOptionsT
|
||||
def Pack(self, builder):
|
||||
if self.fixedAnchorsSchema is not None:
|
||||
fixedAnchorsSchema = self.fixedAnchorsSchema.Pack(builder)
|
||||
SsdAnchorsOptionsStart(builder)
|
||||
if self.fixedAnchorsSchema is not None:
|
||||
SsdAnchorsOptionsAddFixedAnchorsSchema(builder, fixedAnchorsSchema)
|
||||
ssdAnchorsOptions = SsdAnchorsOptionsEnd(builder)
|
||||
return ssdAnchorsOptions
|
||||
|
||||
|
||||
class TensorsDecodingOptions(object):
|
||||
__slots__ = ['_tab']
|
||||
|
||||
@classmethod
|
||||
def GetRootAs(cls, buf, offset=0):
|
||||
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
|
||||
x = TensorsDecodingOptions()
|
||||
x.Init(buf, n + offset)
|
||||
return x
|
||||
|
||||
@classmethod
|
||||
def GetRootAsTensorsDecodingOptions(cls, buf, offset=0):
|
||||
"""This method is deprecated. Please switch to GetRootAs."""
|
||||
return cls.GetRootAs(buf, offset)
|
||||
@classmethod
|
||||
def TensorsDecodingOptionsBufferHasIdentifier(cls, buf, offset, size_prefixed=False):
|
||||
return flatbuffers.util.BufferHasIdentifier(buf, offset, b"\x56\x30\x30\x31", size_prefixed=size_prefixed)
|
||||
|
||||
# TensorsDecodingOptions
|
||||
def Init(self, buf, pos):
|
||||
self._tab = flatbuffers.table.Table(buf, pos)
|
||||
|
||||
# TensorsDecodingOptions
|
||||
def NumClasses(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
|
||||
if o != 0:
|
||||
return self._tab.Get(flatbuffers.number_types.Int32Flags, o + self._tab.Pos)
|
||||
return 0
|
||||
|
||||
# TensorsDecodingOptions
|
||||
def NumBoxes(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
|
||||
if o != 0:
|
||||
return self._tab.Get(flatbuffers.number_types.Int32Flags, o + self._tab.Pos)
|
||||
return 0
|
||||
|
||||
# TensorsDecodingOptions
|
||||
def NumCoords(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
|
||||
if o != 0:
|
||||
return self._tab.Get(flatbuffers.number_types.Int32Flags, o + self._tab.Pos)
|
||||
return 0
|
||||
|
||||
# TensorsDecodingOptions
|
||||
def KeypointCoordOffset(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
|
||||
if o != 0:
|
||||
return self._tab.Get(flatbuffers.number_types.Int32Flags, o + self._tab.Pos)
|
||||
return 0
|
||||
|
||||
# TensorsDecodingOptions
|
||||
def NumKeypoints(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
|
||||
if o != 0:
|
||||
return self._tab.Get(flatbuffers.number_types.Int32Flags, o + self._tab.Pos)
|
||||
return 0
|
||||
|
||||
# TensorsDecodingOptions
|
||||
def NumValuesPerKeypoint(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
|
||||
if o != 0:
|
||||
return self._tab.Get(flatbuffers.number_types.Int32Flags, o + self._tab.Pos)
|
||||
return 0
|
||||
|
||||
# TensorsDecodingOptions
|
||||
def XScale(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16))
|
||||
if o != 0:
|
||||
return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos)
|
||||
return 0.0
|
||||
|
||||
# TensorsDecodingOptions
|
||||
def YScale(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18))
|
||||
if o != 0:
|
||||
return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos)
|
||||
return 0.0
|
||||
|
||||
# TensorsDecodingOptions
|
||||
def WScale(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(20))
|
||||
if o != 0:
|
||||
return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos)
|
||||
return 0.0
|
||||
|
||||
# TensorsDecodingOptions
|
||||
def HScale(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(22))
|
||||
if o != 0:
|
||||
return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos)
|
||||
return 0.0
|
||||
|
||||
# TensorsDecodingOptions
|
||||
def ApplyExponentialOnBoxSize(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(24))
|
||||
if o != 0:
|
||||
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
|
||||
return False
|
||||
|
||||
# TensorsDecodingOptions
|
||||
def SigmoidScore(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(26))
|
||||
if o != 0:
|
||||
return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
|
||||
return False
|
||||
|
||||
def TensorsDecodingOptionsStart(builder):
|
||||
builder.StartObject(12)
|
||||
|
||||
def TensorsDecodingOptionsAddNumClasses(builder, numClasses):
|
||||
builder.PrependInt32Slot(0, numClasses, 0)
|
||||
|
||||
def TensorsDecodingOptionsAddNumBoxes(builder, numBoxes):
|
||||
builder.PrependInt32Slot(1, numBoxes, 0)
|
||||
|
||||
def TensorsDecodingOptionsAddNumCoords(builder, numCoords):
|
||||
builder.PrependInt32Slot(2, numCoords, 0)
|
||||
|
||||
def TensorsDecodingOptionsAddKeypointCoordOffset(builder, keypointCoordOffset):
|
||||
builder.PrependInt32Slot(3, keypointCoordOffset, 0)
|
||||
|
||||
def TensorsDecodingOptionsAddNumKeypoints(builder, numKeypoints):
|
||||
builder.PrependInt32Slot(4, numKeypoints, 0)
|
||||
|
||||
def TensorsDecodingOptionsAddNumValuesPerKeypoint(builder, numValuesPerKeypoint):
|
||||
builder.PrependInt32Slot(5, numValuesPerKeypoint, 0)
|
||||
|
||||
def TensorsDecodingOptionsAddXScale(builder, xScale):
|
||||
builder.PrependFloat32Slot(6, xScale, 0.0)
|
||||
|
||||
def TensorsDecodingOptionsAddYScale(builder, yScale):
|
||||
builder.PrependFloat32Slot(7, yScale, 0.0)
|
||||
|
||||
def TensorsDecodingOptionsAddWScale(builder, wScale):
|
||||
builder.PrependFloat32Slot(8, wScale, 0.0)
|
||||
|
||||
def TensorsDecodingOptionsAddHScale(builder, hScale):
|
||||
builder.PrependFloat32Slot(9, hScale, 0.0)
|
||||
|
||||
def TensorsDecodingOptionsAddApplyExponentialOnBoxSize(builder, applyExponentialOnBoxSize):
|
||||
builder.PrependBoolSlot(10, applyExponentialOnBoxSize, 0)
|
||||
|
||||
def TensorsDecodingOptionsAddSigmoidScore(builder, sigmoidScore):
|
||||
builder.PrependBoolSlot(11, sigmoidScore, 0)
|
||||
|
||||
def TensorsDecodingOptionsEnd(builder):
|
||||
return builder.EndObject()
|
||||
|
||||
|
||||
|
||||
class TensorsDecodingOptionsT(object):
|
||||
|
||||
# TensorsDecodingOptionsT
|
||||
def __init__(self):
|
||||
self.numClasses = 0 # type: int
|
||||
self.numBoxes = 0 # type: int
|
||||
self.numCoords = 0 # type: int
|
||||
self.keypointCoordOffset = 0 # type: int
|
||||
self.numKeypoints = 0 # type: int
|
||||
self.numValuesPerKeypoint = 0 # type: int
|
||||
self.xScale = 0.0 # type: float
|
||||
self.yScale = 0.0 # type: float
|
||||
self.wScale = 0.0 # type: float
|
||||
self.hScale = 0.0 # type: float
|
||||
self.applyExponentialOnBoxSize = False # type: bool
|
||||
self.sigmoidScore = False # type: bool
|
||||
|
||||
@classmethod
|
||||
def InitFromBuf(cls, buf, pos):
|
||||
tensorsDecodingOptions = TensorsDecodingOptions()
|
||||
tensorsDecodingOptions.Init(buf, pos)
|
||||
return cls.InitFromObj(tensorsDecodingOptions)
|
||||
|
||||
@classmethod
|
||||
def InitFromPackedBuf(cls, buf, pos=0):
|
||||
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, pos)
|
||||
return cls.InitFromBuf(buf, pos+n)
|
||||
|
||||
@classmethod
|
||||
def InitFromObj(cls, tensorsDecodingOptions):
|
||||
x = TensorsDecodingOptionsT()
|
||||
x._UnPack(tensorsDecodingOptions)
|
||||
return x
|
||||
|
||||
# TensorsDecodingOptionsT
|
||||
def _UnPack(self, tensorsDecodingOptions):
|
||||
if tensorsDecodingOptions is None:
|
||||
return
|
||||
self.numClasses = tensorsDecodingOptions.NumClasses()
|
||||
self.numBoxes = tensorsDecodingOptions.NumBoxes()
|
||||
self.numCoords = tensorsDecodingOptions.NumCoords()
|
||||
self.keypointCoordOffset = tensorsDecodingOptions.KeypointCoordOffset()
|
||||
self.numKeypoints = tensorsDecodingOptions.NumKeypoints()
|
||||
self.numValuesPerKeypoint = tensorsDecodingOptions.NumValuesPerKeypoint()
|
||||
self.xScale = tensorsDecodingOptions.XScale()
|
||||
self.yScale = tensorsDecodingOptions.YScale()
|
||||
self.wScale = tensorsDecodingOptions.WScale()
|
||||
self.hScale = tensorsDecodingOptions.HScale()
|
||||
self.applyExponentialOnBoxSize = tensorsDecodingOptions.ApplyExponentialOnBoxSize()
|
||||
self.sigmoidScore = tensorsDecodingOptions.SigmoidScore()
|
||||
|
||||
# TensorsDecodingOptionsT
|
||||
def Pack(self, builder):
|
||||
TensorsDecodingOptionsStart(builder)
|
||||
TensorsDecodingOptionsAddNumClasses(builder, self.numClasses)
|
||||
TensorsDecodingOptionsAddNumBoxes(builder, self.numBoxes)
|
||||
TensorsDecodingOptionsAddNumCoords(builder, self.numCoords)
|
||||
TensorsDecodingOptionsAddKeypointCoordOffset(builder, self.keypointCoordOffset)
|
||||
TensorsDecodingOptionsAddNumKeypoints(builder, self.numKeypoints)
|
||||
TensorsDecodingOptionsAddNumValuesPerKeypoint(builder, self.numValuesPerKeypoint)
|
||||
TensorsDecodingOptionsAddXScale(builder, self.xScale)
|
||||
TensorsDecodingOptionsAddYScale(builder, self.yScale)
|
||||
TensorsDecodingOptionsAddWScale(builder, self.wScale)
|
||||
TensorsDecodingOptionsAddHScale(builder, self.hScale)
|
||||
TensorsDecodingOptionsAddApplyExponentialOnBoxSize(builder, self.applyExponentialOnBoxSize)
|
||||
TensorsDecodingOptionsAddSigmoidScore(builder, self.sigmoidScore)
|
||||
tensorsDecodingOptions = TensorsDecodingOptionsEnd(builder)
|
||||
return tensorsDecodingOptions
|
||||
|
||||
|
||||
class ObjectDetectorOptions(object):
|
||||
__slots__ = ['_tab']
|
||||
|
||||
@classmethod
|
||||
def GetRootAs(cls, buf, offset=0):
|
||||
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
|
||||
x = ObjectDetectorOptions()
|
||||
x.Init(buf, n + offset)
|
||||
return x
|
||||
|
||||
@classmethod
|
||||
def GetRootAsObjectDetectorOptions(cls, buf, offset=0):
|
||||
"""This method is deprecated. Please switch to GetRootAs."""
|
||||
return cls.GetRootAs(buf, offset)
|
||||
@classmethod
|
||||
def ObjectDetectorOptionsBufferHasIdentifier(cls, buf, offset, size_prefixed=False):
|
||||
return flatbuffers.util.BufferHasIdentifier(buf, offset, b"\x56\x30\x30\x31", size_prefixed=size_prefixed)
|
||||
|
||||
# ObjectDetectorOptions
|
||||
def Init(self, buf, pos):
|
||||
self._tab = flatbuffers.table.Table(buf, pos)
|
||||
|
||||
# ObjectDetectorOptions
|
||||
def MinParserVersion(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
|
||||
if o != 0:
|
||||
return self._tab.String(o + self._tab.Pos)
|
||||
return None
|
||||
|
||||
# ObjectDetectorOptions
|
||||
def SsdAnchorsOptions(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
|
||||
if o != 0:
|
||||
x = self._tab.Indirect(o + self._tab.Pos)
|
||||
obj = SsdAnchorsOptions()
|
||||
obj.Init(self._tab.Bytes, x)
|
||||
return obj
|
||||
return None
|
||||
|
||||
# ObjectDetectorOptions
|
||||
def TensorsDecodingOptions(self):
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
|
||||
if o != 0:
|
||||
x = self._tab.Indirect(o + self._tab.Pos)
|
||||
obj = TensorsDecodingOptions()
|
||||
obj.Init(self._tab.Bytes, x)
|
||||
return obj
|
||||
return None
|
||||
|
||||
def ObjectDetectorOptionsStart(builder):
|
||||
builder.StartObject(3)
|
||||
|
||||
def ObjectDetectorOptionsAddMinParserVersion(builder, minParserVersion):
|
||||
builder.PrependUOffsetTRelativeSlot(0, flatbuffers.number_types.UOffsetTFlags.py_type(minParserVersion), 0)
|
||||
|
||||
def ObjectDetectorOptionsAddSsdAnchorsOptions(builder, ssdAnchorsOptions):
|
||||
builder.PrependUOffsetTRelativeSlot(1, flatbuffers.number_types.UOffsetTFlags.py_type(ssdAnchorsOptions), 0)
|
||||
|
||||
def ObjectDetectorOptionsAddTensorsDecodingOptions(builder, tensorsDecodingOptions):
|
||||
builder.PrependUOffsetTRelativeSlot(2, flatbuffers.number_types.UOffsetTFlags.py_type(tensorsDecodingOptions), 0)
|
||||
|
||||
def ObjectDetectorOptionsEnd(builder):
|
||||
return builder.EndObject()
|
||||
|
||||
|
||||
try:
|
||||
from typing import Optional
|
||||
except:
|
||||
pass
|
||||
|
||||
class ObjectDetectorOptionsT(object):
|
||||
|
||||
# ObjectDetectorOptionsT
|
||||
def __init__(self):
|
||||
self.minParserVersion = None # type: str
|
||||
self.ssdAnchorsOptions = None # type: Optional[SsdAnchorsOptionsT]
|
||||
self.tensorsDecodingOptions = None # type: Optional[TensorsDecodingOptionsT]
|
||||
|
||||
@classmethod
|
||||
def InitFromBuf(cls, buf, pos):
|
||||
objectDetectorOptions = ObjectDetectorOptions()
|
||||
objectDetectorOptions.Init(buf, pos)
|
||||
return cls.InitFromObj(objectDetectorOptions)
|
||||
|
||||
@classmethod
|
||||
def InitFromPackedBuf(cls, buf, pos=0):
|
||||
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, pos)
|
||||
return cls.InitFromBuf(buf, pos+n)
|
||||
|
||||
@classmethod
|
||||
def InitFromObj(cls, objectDetectorOptions):
|
||||
x = ObjectDetectorOptionsT()
|
||||
x._UnPack(objectDetectorOptions)
|
||||
return x
|
||||
|
||||
# ObjectDetectorOptionsT
|
||||
def _UnPack(self, objectDetectorOptions):
|
||||
if objectDetectorOptions is None:
|
||||
return
|
||||
self.minParserVersion = objectDetectorOptions.MinParserVersion()
|
||||
if objectDetectorOptions.SsdAnchorsOptions() is not None:
|
||||
self.ssdAnchorsOptions = SsdAnchorsOptionsT.InitFromObj(objectDetectorOptions.SsdAnchorsOptions())
|
||||
if objectDetectorOptions.TensorsDecodingOptions() is not None:
|
||||
self.tensorsDecodingOptions = TensorsDecodingOptionsT.InitFromObj(objectDetectorOptions.TensorsDecodingOptions())
|
||||
|
||||
# ObjectDetectorOptionsT
|
||||
def Pack(self, builder):
|
||||
if self.minParserVersion is not None:
|
||||
minParserVersion = builder.CreateString(self.minParserVersion)
|
||||
if self.ssdAnchorsOptions is not None:
|
||||
ssdAnchorsOptions = self.ssdAnchorsOptions.Pack(builder)
|
||||
if self.tensorsDecodingOptions is not None:
|
||||
tensorsDecodingOptions = self.tensorsDecodingOptions.Pack(builder)
|
||||
ObjectDetectorOptionsStart(builder)
|
||||
if self.minParserVersion is not None:
|
||||
ObjectDetectorOptionsAddMinParserVersion(builder, minParserVersion)
|
||||
if self.ssdAnchorsOptions is not None:
|
||||
ObjectDetectorOptionsAddSsdAnchorsOptions(builder, ssdAnchorsOptions)
|
||||
if self.tensorsDecodingOptions is not None:
|
||||
ObjectDetectorOptionsAddTensorsDecodingOptions(builder, tensorsDecodingOptions)
|
||||
objectDetectorOptions = ObjectDetectorOptionsEnd(builder)
|
||||
return objectDetectorOptions
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""MediaPipe Tasks API."""
|
||||
|
||||
from . import audio
|
||||
from . import components
|
||||
from . import core
|
||||
from . import genai
|
||||
from . import text
|
||||
from . import vision
|
||||
|
||||
BaseOptions = core.base_options.BaseOptions
|
||||
|
||||
# Remove unnecessary modules to avoid duplication in API docs.
|
||||
del core
|
||||
@@ -0,0 +1,28 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""MediaPipe Tasks Audio API."""
|
||||
|
||||
import mediapipe.tasks.python.audio.core
|
||||
import mediapipe.tasks.python.audio.audio_classifier
|
||||
|
||||
AudioClassifier = audio_classifier.AudioClassifier
|
||||
AudioClassifierOptions = audio_classifier.AudioClassifierOptions
|
||||
AudioClassifierResult = audio_classifier.AudioClassifierResult
|
||||
RunningMode = core.audio_task_running_mode.AudioTaskRunningMode
|
||||
|
||||
# Remove unnecessary modules to avoid duplication in API docs.
|
||||
del audio_classifier
|
||||
del core
|
||||
del mediapipe
|
||||
@@ -0,0 +1,456 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""MediaPipe audio classifier task."""
|
||||
|
||||
import ctypes
|
||||
import dataclasses
|
||||
from typing import Callable, Optional
|
||||
|
||||
from mediapipe.tasks.python.audio.core import audio_record
|
||||
from mediapipe.tasks.python.audio.core import audio_task_running_mode
|
||||
from mediapipe.tasks.python.components.containers import audio_data
|
||||
from mediapipe.tasks.python.components.containers import audio_data_c
|
||||
from mediapipe.tasks.python.components.containers import classification_result
|
||||
from mediapipe.tasks.python.components.containers import classification_result_c
|
||||
from mediapipe.tasks.python.components.processors import classifier_options as classifier_options_lib
|
||||
from mediapipe.tasks.python.components.processors import classifier_options_c
|
||||
from mediapipe.tasks.python.core import async_result_dispatcher
|
||||
from mediapipe.tasks.python.core import base_options as base_options_lib
|
||||
from mediapipe.tasks.python.core import base_options_c
|
||||
from mediapipe.tasks.python.core import mediapipe_c_bindings
|
||||
from mediapipe.tasks.python.core import mediapipe_c_utils
|
||||
from mediapipe.tasks.python.core import serial_dispatcher
|
||||
from mediapipe.tasks.python.core.optional_dependencies import doc_controls
|
||||
|
||||
AudioClassifierResult = classification_result.ClassificationResult
|
||||
_AudioData = audio_data.AudioData
|
||||
_BaseOptions = base_options_lib.BaseOptions
|
||||
_RunningMode = audio_task_running_mode.AudioTaskRunningMode
|
||||
_MICRO_SECONDS_PER_MILLISECOND = 1000
|
||||
_AsyncResultDispatcher = async_result_dispatcher.AsyncResultDispatcher
|
||||
_LiveStreamPacket = async_result_dispatcher.LiveStreamPacket
|
||||
|
||||
|
||||
class AudioClassifierResultC(ctypes.Structure):
|
||||
"""The C representation of a list of audio classification results."""
|
||||
|
||||
_fields_ = [
|
||||
(
|
||||
'results',
|
||||
ctypes.POINTER(classification_result_c.ClassificationResultC),
|
||||
),
|
||||
('results_count', ctypes.c_int),
|
||||
]
|
||||
|
||||
_C_TYPES_RESULT_CALLBACK = ctypes.CFUNCTYPE(
|
||||
None, ctypes.c_int32, ctypes.POINTER(AudioClassifierResultC)
|
||||
)
|
||||
|
||||
|
||||
class AudioClassifierOptionsC(ctypes.Structure):
|
||||
"""The audio classifier options used in the C API."""
|
||||
|
||||
_fields_ = [
|
||||
('base_options', base_options_c.BaseOptionsC),
|
||||
('classifier_options', classifier_options_c.ClassifierOptionsC),
|
||||
('running_mode', ctypes.c_int),
|
||||
('result_callback', _C_TYPES_RESULT_CALLBACK),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
@doc_controls.do_not_generate_docs
|
||||
def from_c_options(
|
||||
cls,
|
||||
base_options: base_options_c.BaseOptionsC,
|
||||
classifier_options: classifier_options_c.ClassifierOptionsC,
|
||||
running_mode: _RunningMode,
|
||||
result_callback: _C_TYPES_RESULT_CALLBACK,
|
||||
) -> 'AudioClassifierOptionsC':
|
||||
"""Creates an AudioClassifierOptionsC object from the given options."""
|
||||
return cls(
|
||||
base_options=base_options,
|
||||
classifier_options=classifier_options,
|
||||
running_mode=running_mode.ctype,
|
||||
result_callback=result_callback,
|
||||
)
|
||||
|
||||
|
||||
_CTYPES_SIGNATURES = (
|
||||
mediapipe_c_utils.CStatusFunction(
|
||||
'MpAudioClassifierCreate',
|
||||
(
|
||||
ctypes.POINTER(AudioClassifierOptionsC),
|
||||
ctypes.POINTER(ctypes.c_void_p),
|
||||
),
|
||||
),
|
||||
mediapipe_c_utils.CStatusFunction(
|
||||
'MpAudioClassifierClassify',
|
||||
(
|
||||
ctypes.c_void_p,
|
||||
ctypes.POINTER(audio_data_c.AudioDataC),
|
||||
ctypes.POINTER(AudioClassifierResultC),
|
||||
),
|
||||
),
|
||||
mediapipe_c_utils.CStatusFunction(
|
||||
'MpAudioClassifierClassifyAsync',
|
||||
(
|
||||
ctypes.c_void_p,
|
||||
ctypes.POINTER(audio_data_c.AudioDataC),
|
||||
ctypes.c_int64,
|
||||
),
|
||||
),
|
||||
mediapipe_c_utils.CFunction(
|
||||
'MpAudioClassifierCloseResult',
|
||||
[ctypes.POINTER(AudioClassifierResultC)],
|
||||
None,
|
||||
),
|
||||
mediapipe_c_utils.CStatusFunction(
|
||||
'MpAudioClassifierClose',
|
||||
(ctypes.c_void_p,),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AudioClassifierOptions:
|
||||
"""Options for the audio classifier task.
|
||||
|
||||
Attributes:
|
||||
base_options: Base options for the audio classifier task.
|
||||
running_mode: The running mode of the task. Default to the audio clips mode.
|
||||
Audio classifier task has two running modes: 1) The audio clips mode for
|
||||
running classification on independent audio clips. 2) The audio stream
|
||||
mode for running classification on the audio stream, such as from
|
||||
microphone. In this mode, the "result_callback" below must be specified
|
||||
to receive the classification results asynchronously.
|
||||
display_names_locale: The locale to use for display names specified through
|
||||
the TFLite Model Metadata.
|
||||
max_results: The maximum number of top-scored classification results to
|
||||
return.
|
||||
score_threshold: Overrides the ones provided in the model metadata. Results
|
||||
below this value are rejected.
|
||||
category_allowlist: Allowlist of category names. If non-empty,
|
||||
classification results whose category name is not in this set will be
|
||||
filtered out. Duplicate or unknown category names are ignored. Mutually
|
||||
exclusive with `category_denylist`.
|
||||
category_denylist: Denylist of category names. If non-empty, classification
|
||||
results whose category name is in this set will be filtered out. Duplicate
|
||||
or unknown category names are ignored. Mutually exclusive with
|
||||
`category_allowlist`.
|
||||
result_callback: The user-defined result callback for processing audio
|
||||
stream data. The result callback should only be specified when the running
|
||||
mode is set to the audio stream mode.
|
||||
"""
|
||||
base_options: _BaseOptions
|
||||
running_mode: _RunningMode = _RunningMode.AUDIO_CLIPS
|
||||
display_names_locale: Optional[str] = None
|
||||
max_results: Optional[int] = None
|
||||
score_threshold: Optional[float] = None
|
||||
category_allowlist: Optional[list[str]] = None
|
||||
category_denylist: Optional[list[str]] = None
|
||||
result_callback: Optional[Callable[[AudioClassifierResult, int], None]] = None
|
||||
|
||||
|
||||
class AudioClassifier:
|
||||
"""Class that performs audio classification on audio data.
|
||||
|
||||
This API expects a TFLite model with mandatory TFLite Model Metadata that
|
||||
contains the mandatory AudioProperties of the solo input audio tensor and the
|
||||
optional (but recommended) category labels as AssociatedFiles with type
|
||||
TENSOR_AXIS_LABELS per output classification tensor.
|
||||
|
||||
Input tensor:
|
||||
(kTfLiteFloat32)
|
||||
- input audio buffer of size `[batch * samples]`.
|
||||
- batch inference is not supported (`batch` is required to be 1).
|
||||
- for multi-channel models, the channels must be interleaved.
|
||||
At least one output tensor with:
|
||||
(kTfLiteFloat32)
|
||||
- `[1 x N]` array with `N` represents the number of categories.
|
||||
- optional (but recommended) category labels as AssociatedFiles with type
|
||||
TENSOR_AXIS_LABELS, containing one label per line. The first such
|
||||
AssociatedFile (if any) is used to fill the `category_name` field of the
|
||||
results. The `display_name` field is filled from the AssociatedFile (if
|
||||
any) whose locale matches the `display_names_locale` field of the
|
||||
`AudioClassifierOptions` used at creation time ("en" by default, i.e.
|
||||
English). If none of these are available, only the `index` field of the
|
||||
results will be filled.
|
||||
"""
|
||||
_lib: serial_dispatcher.SerialDispatcher
|
||||
_handle: ctypes.c_void_p
|
||||
_dispatcher: _AsyncResultDispatcher
|
||||
_async_callback: _C_TYPES_RESULT_CALLBACK
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
lib: serial_dispatcher.SerialDispatcher,
|
||||
handle: ctypes.c_void_p,
|
||||
dispatcher: _AsyncResultDispatcher,
|
||||
async_callback: _C_TYPES_RESULT_CALLBACK,
|
||||
):
|
||||
"""Initializes the AudioClassifier instance.
|
||||
|
||||
Args:
|
||||
lib: The serial dispatcher for the audio classifier task.
|
||||
handle: The handle to the audio classifier task.
|
||||
dispatcher: The async result dispatcher for the audio classifier task.
|
||||
async_callback: The C callback for processing audio stream data.
|
||||
"""
|
||||
self._lib = lib
|
||||
self._handle = handle
|
||||
self._dispatcher = dispatcher
|
||||
self._async_callback = async_callback
|
||||
|
||||
@classmethod
|
||||
def create_from_model_path(cls, model_path: str) -> 'AudioClassifier':
|
||||
"""Creates an `AudioClassifier` object from a TensorFlow Lite model and the default `AudioClassifierOptions`.
|
||||
|
||||
Note that the created `AudioClassifier` instance is in audio clips mode, for
|
||||
classifying on independent audio clips.
|
||||
|
||||
Args:
|
||||
model_path: Path to the model.
|
||||
|
||||
Returns:
|
||||
`AudioClassifier` object that's created from the model file and the
|
||||
default `AudioClassifierOptions`.
|
||||
|
||||
Raises:
|
||||
ValueError: If failed to create `AudioClassifier` object from the provided
|
||||
file such as invalid file path.
|
||||
RuntimeError: If other types of error occurred.
|
||||
"""
|
||||
base_options = _BaseOptions(model_asset_path=model_path)
|
||||
options = AudioClassifierOptions(
|
||||
base_options=base_options, running_mode=_RunningMode.AUDIO_CLIPS
|
||||
)
|
||||
return cls.create_from_options(options)
|
||||
|
||||
@classmethod
|
||||
def create_from_options(
|
||||
cls, options: AudioClassifierOptions
|
||||
) -> 'AudioClassifier':
|
||||
"""Creates the `AudioClassifier` object from audio classifier options.
|
||||
|
||||
Args:
|
||||
options: Options for the audio classifier task.
|
||||
|
||||
Returns:
|
||||
`AudioClassifier` object that's created from `options`.
|
||||
|
||||
Raises:
|
||||
ValueError: If failed to create `AudioClassifier` object from
|
||||
`AudioClassifierOptions` such as missing the model.
|
||||
RuntimeError: If other types of error occurred.
|
||||
"""
|
||||
lib = mediapipe_c_bindings.load_shared_library(_CTYPES_SIGNATURES)
|
||||
|
||||
def convert_result(c_result_ptr: ctypes.POINTER(AudioClassifierResultC)):
|
||||
c_result = c_result_ptr[0]
|
||||
if c_result.results_count == 0:
|
||||
raise RuntimeError('No results returned from audio classifier.')
|
||||
py_result = AudioClassifierResult.from_ctypes(c_result.results[0])
|
||||
return (py_result, py_result.timestamp_ms)
|
||||
|
||||
dispatcher = _AsyncResultDispatcher(converter=convert_result)
|
||||
|
||||
c_callback = dispatcher.wrap_callback(
|
||||
options.result_callback, _C_TYPES_RESULT_CALLBACK
|
||||
)
|
||||
ctypes_options = AudioClassifierOptionsC.from_c_options(
|
||||
base_options=options.base_options.to_ctypes(),
|
||||
classifier_options=classifier_options_c.convert_to_classifier_options_c(
|
||||
classifier_options_lib.ClassifierOptions(
|
||||
score_threshold=options.score_threshold,
|
||||
category_allowlist=options.category_allowlist,
|
||||
category_denylist=options.category_denylist,
|
||||
display_names_locale=options.display_names_locale,
|
||||
max_results=options.max_results,
|
||||
)
|
||||
),
|
||||
running_mode=options.running_mode,
|
||||
result_callback=c_callback,
|
||||
)
|
||||
|
||||
classifier_handle_ptr = ctypes.c_void_p()
|
||||
lib.MpAudioClassifierCreate(
|
||||
ctypes.byref(ctypes_options), ctypes.byref(classifier_handle_ptr)
|
||||
)
|
||||
return AudioClassifier(
|
||||
lib=lib,
|
||||
handle=classifier_handle_ptr,
|
||||
dispatcher=dispatcher,
|
||||
async_callback=c_callback,
|
||||
)
|
||||
|
||||
def classify(self, audio_clip: _AudioData) -> list[AudioClassifierResult]:
|
||||
"""Performs audio classification on the provided audio clip.
|
||||
|
||||
The audio clip is represented as a MediaPipe AudioData. The method accepts
|
||||
audio clips with various length and audio sample rate. It's required to
|
||||
provide the corresponding audio sample rate within the `AudioData` object.
|
||||
|
||||
The input audio clip may be longer than what the model is able to process
|
||||
in a single inference. When this occurs, the input audio clip is split into
|
||||
multiple chunks starting at different timestamps. For this reason, this
|
||||
function returns a vector of ClassificationResult objects, each associated
|
||||
ith a timestamp corresponding to the start (in milliseconds) of the chunk
|
||||
data that was classified, e.g:
|
||||
|
||||
ClassificationResult #0 (first chunk of data):
|
||||
timestamp_ms: 0 (starts at 0ms)
|
||||
classifications #0 (single head model):
|
||||
category #0:
|
||||
category_name: "Speech"
|
||||
score: 0.6
|
||||
category #1:
|
||||
category_name: "Music"
|
||||
score: 0.2
|
||||
ClassificationResult #1 (second chunk of data):
|
||||
timestamp_ms: 800 (starts at 800ms)
|
||||
classifications #0 (single head model):
|
||||
category #0:
|
||||
category_name: "Speech"
|
||||
score: 0.5
|
||||
category #1:
|
||||
category_name: "Silence"
|
||||
score: 0.1
|
||||
|
||||
Args:
|
||||
audio_clip: MediaPipe AudioData.
|
||||
|
||||
Returns:
|
||||
An `AudioClassifierResult` object that contains a list of
|
||||
classification result objects, each associated with a timestamp
|
||||
corresponding to the start (in milliseconds) of the chunk data that was
|
||||
classified.
|
||||
|
||||
Raises:
|
||||
ValueError: If any of the input arguments is invalid, such as the sample
|
||||
rate is not provided in the `AudioData` object.
|
||||
RuntimeError: If audio classification failed to run.
|
||||
"""
|
||||
if not audio_clip.audio_format.sample_rate:
|
||||
raise ValueError('Must provide the audio sample rate in audio data.')
|
||||
|
||||
c_result = AudioClassifierResultC()
|
||||
self._lib.MpAudioClassifierClassify(
|
||||
self._handle,
|
||||
audio_clip.to_ctypes(),
|
||||
ctypes.byref(c_result),
|
||||
)
|
||||
py_result = [
|
||||
AudioClassifierResult.from_ctypes(c_result.results[i])
|
||||
for i in range(c_result.results_count)
|
||||
]
|
||||
self._lib.MpAudioClassifierCloseResult(ctypes.byref(c_result))
|
||||
return py_result
|
||||
|
||||
def classify_async(self, audio_block: _AudioData, timestamp_ms: int) -> None:
|
||||
"""Sends audio data (a block in a continuous audio stream) to perform audio classification.
|
||||
|
||||
Only use this method when the AudioClassifier is created with the audio
|
||||
stream running mode. The input timestamps should be monotonically increasing
|
||||
for adjacent calls of this method. This method will return immediately after
|
||||
the input audio data is accepted. The results will be available via the
|
||||
`result_callback` provided in the `AudioClassifierOptions`. The
|
||||
`classify_async` method is designed to process auido stream data such as
|
||||
microphone input.
|
||||
|
||||
The input audio data may be longer than what the model is able to process
|
||||
in a single inference. When this occurs, the input audio block is split
|
||||
into multiple chunks. For this reason, the callback may be called multiple
|
||||
times (once per chunk) for each call to this function.
|
||||
|
||||
The `result_callback` provides:
|
||||
- An `AudioClassifierResult` object that contains a list of
|
||||
classifications.
|
||||
- The input timestamp in milliseconds.
|
||||
|
||||
Args:
|
||||
audio_block: MediaPipe AudioData.
|
||||
timestamp_ms: The timestamp of the input audio data in milliseconds.
|
||||
|
||||
Raises:
|
||||
ValueError: If any of the followings:
|
||||
1) The sample rate is not provided in the `AudioData` object or the
|
||||
provided sample rate is inconsistent with the previously received.
|
||||
2) The current input timestamp is smaller than what the audio
|
||||
classifier has already processed.
|
||||
"""
|
||||
if not audio_block.audio_format.sample_rate:
|
||||
raise ValueError('Must provide the audio sample rate in audio data.')
|
||||
|
||||
self._lib.MpAudioClassifierClassifyAsync(
|
||||
self._handle,
|
||||
audio_block.to_ctypes(),
|
||||
timestamp_ms,
|
||||
)
|
||||
|
||||
def create_audio_record(
|
||||
self, num_channels: int, sample_rate: int, required_input_buffer_size: int
|
||||
) -> audio_record.AudioRecord:
|
||||
"""Creates an AudioRecord instance to record audio stream.
|
||||
|
||||
The returned AudioRecord instance is initialized and client needs to call
|
||||
the appropriate method to start recording.
|
||||
|
||||
Note that MediaPipe Audio tasks will up/down sample automatically to fit the
|
||||
sample rate required by the model. The default sample rate of the MediaPipe
|
||||
pretrained audio model, Yamnet is 16kHz.
|
||||
|
||||
Args:
|
||||
num_channels: The number of audio channels.
|
||||
sample_rate: The audio sample rate.
|
||||
required_input_buffer_size: The required input buffer size in number of
|
||||
float elements.
|
||||
|
||||
Returns:
|
||||
An AudioRecord instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If there's a problem creating the AudioRecord instance.
|
||||
"""
|
||||
return audio_record.AudioRecord(
|
||||
num_channels, sample_rate, required_input_buffer_size
|
||||
)
|
||||
|
||||
def close(self):
|
||||
"""Shuts down the MediaPipe task instance."""
|
||||
if self._handle:
|
||||
self._lib.MpAudioClassifierClose(self._handle)
|
||||
self._handle = None
|
||||
self._dispatcher.close()
|
||||
self._lib.close()
|
||||
|
||||
def __enter__(self):
|
||||
"""Returns `self` upon entering the runtime context."""
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
"""Shuts down the MediaPipe task instance on exit of the context manager.
|
||||
|
||||
Args:
|
||||
exc_type: The exception type that caused the exit.
|
||||
exc_value: The exception value that caused the exit.
|
||||
traceback: The exception traceback that caused the exit.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the MediaPipe TextClassifier task failed to close.
|
||||
"""
|
||||
del exc_type, exc_value, traceback # Unused.
|
||||
self.close()
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Copyright 2022 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.
|
||||
"""
|
||||
@@ -0,0 +1,125 @@
|
||||
# Copyright 2023 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""A module to record audio in a streaming basis."""
|
||||
import threading
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
import sounddevice as sd
|
||||
except OSError as oe:
|
||||
sd = None
|
||||
sd_error = oe
|
||||
except ImportError as ie:
|
||||
sd = None
|
||||
sd_error = ie
|
||||
|
||||
|
||||
class AudioRecord(object):
|
||||
"""A class to record audio in a streaming basis."""
|
||||
|
||||
def __init__(
|
||||
self, channels: int, sampling_rate: int, buffer_size: int
|
||||
) -> None:
|
||||
"""Creates an AudioRecord instance.
|
||||
|
||||
Args:
|
||||
channels: Number of input channels.
|
||||
sampling_rate: Sampling rate in Hertz.
|
||||
buffer_size: Size of the ring buffer in number of samples.
|
||||
|
||||
Raises:
|
||||
ValueError: if any of the arguments is non-positive.
|
||||
ImportError: if failed to import `sounddevice`.
|
||||
OSError: if failed to load `PortAudio`.
|
||||
"""
|
||||
if sd is None:
|
||||
raise sd_error
|
||||
|
||||
if channels <= 0:
|
||||
raise ValueError('channels must be positive.')
|
||||
if sampling_rate <= 0:
|
||||
raise ValueError('sampling_rate must be positive.')
|
||||
if buffer_size <= 0:
|
||||
raise ValueError('buffer_size must be positive.')
|
||||
|
||||
self._audio_buffer = []
|
||||
self._buffer_size = buffer_size
|
||||
self._channels = channels
|
||||
self._sampling_rate = sampling_rate
|
||||
|
||||
# Create a ring buffer to store the input audio.
|
||||
self._buffer = np.zeros([buffer_size, channels], dtype=float)
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def audio_callback(data, *_):
|
||||
"""A callback to receive recorded audio data from sounddevice."""
|
||||
self._lock.acquire()
|
||||
shift = len(data)
|
||||
if shift > buffer_size:
|
||||
self._buffer = np.copy(data[:buffer_size])
|
||||
else:
|
||||
self._buffer = np.roll(self._buffer, -shift, axis=0)
|
||||
self._buffer[-shift:, :] = np.copy(data)
|
||||
self._lock.release()
|
||||
|
||||
# Create an input stream to continuously capture the audio data.
|
||||
self._stream = sd.InputStream(
|
||||
channels=channels,
|
||||
samplerate=sampling_rate,
|
||||
callback=audio_callback,
|
||||
)
|
||||
|
||||
@property
|
||||
def channels(self) -> int:
|
||||
return self._channels
|
||||
|
||||
@property
|
||||
def sampling_rate(self) -> int:
|
||||
return self._sampling_rate
|
||||
|
||||
@property
|
||||
def buffer_size(self) -> int:
|
||||
return self._buffer_size
|
||||
|
||||
def start_recording(self) -> None:
|
||||
"""Starts the audio recording."""
|
||||
# Clear the internal ring buffer.
|
||||
self._buffer.fill(0)
|
||||
|
||||
# Start recording using sounddevice's InputStream.
|
||||
self._stream.start()
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stops the audio recording."""
|
||||
self._stream.stop()
|
||||
|
||||
def read(self, size: int) -> np.ndarray:
|
||||
"""Reads the latest audio data captured in the buffer.
|
||||
|
||||
Args:
|
||||
size: Number of samples to read from the buffer.
|
||||
|
||||
Returns:
|
||||
A NumPy array containing the audio data.
|
||||
|
||||
Raises:
|
||||
ValueError: Raised if `size` is larger than the buffer size.
|
||||
"""
|
||||
if size > self._buffer_size:
|
||||
raise ValueError('Cannot read more samples than the size of the buffer.')
|
||||
elif size <= 0:
|
||||
raise ValueError('Size must be positive.')
|
||||
|
||||
start_index = self._buffer_size - size
|
||||
return np.copy(self._buffer[start_index:])
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""The running mode of MediaPipe Audio Tasks."""
|
||||
|
||||
import enum
|
||||
import types
|
||||
|
||||
from mediapipe.tasks.python.core.optional_dependencies import doc_controls
|
||||
|
||||
# The C API constants that map to the Python enum values.
|
||||
_CTYPE_VALUE_MAP = types.MappingProxyType({
|
||||
'AUDIO_CLIPS': 1,
|
||||
'AUDIO_STREAM': 2,
|
||||
})
|
||||
|
||||
|
||||
class AudioTaskRunningMode(enum.Enum):
|
||||
"""MediaPipe audio task running mode.
|
||||
|
||||
Attributes:
|
||||
AUDIO_CLIPS: The mode for running a mediapipe audio task on independent
|
||||
audio clips.
|
||||
AUDIO_STREAM: The mode for running a mediapipe audio task on an audio
|
||||
stream, such as from microphone.
|
||||
"""
|
||||
AUDIO_CLIPS = 'AUDIO_CLIPS'
|
||||
AUDIO_STREAM = 'AUDIO_STREAM'
|
||||
|
||||
@property
|
||||
@doc_controls.do_not_generate_docs
|
||||
def ctype(self) -> int:
|
||||
"""Generates a C API int object."""
|
||||
ctype_value = _CTYPE_VALUE_MAP.get(self.value)
|
||||
assert (
|
||||
ctype_value is not None
|
||||
), f'Unsupported audio task running mode: {self}'
|
||||
return ctype_value
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright 2023 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Copyright 2023 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Benchmark utils for MediaPipe Tasks."""
|
||||
|
||||
import os
|
||||
import numpy as np
|
||||
|
||||
|
||||
def nth_percentile(inference_times, percentile):
|
||||
"""Calculate the nth percentile of the inference times."""
|
||||
return np.percentile(inference_times, percentile)
|
||||
|
||||
|
||||
def average(inference_times):
|
||||
"""Calculate the average of the inference times."""
|
||||
return np.mean(inference_times)
|
||||
|
||||
|
||||
def get_test_data_path(test_srcdir, file_or_dirname_path: str) -> str:
|
||||
"""Determine the test data path.
|
||||
|
||||
Args:
|
||||
test_srcdir: The path to the test source directory.
|
||||
file_or_dirname_path: The path to the file or directory.
|
||||
|
||||
Returns:
|
||||
The full test data path.
|
||||
"""
|
||||
for directory, subdirs, files in os.walk(test_srcdir):
|
||||
for f in subdirs + files:
|
||||
path = os.path.join(directory, f)
|
||||
if path.endswith(file_or_dirname_path):
|
||||
return path
|
||||
raise ValueError(
|
||||
"No %s in test directory: %s." % (file_or_dirname_path, test_srcdir)
|
||||
)
|
||||
|
||||
|
||||
def get_model_path(custom_model, default_model_path):
|
||||
"""Determine the model path based on the existence of the custom model.
|
||||
|
||||
Args:
|
||||
custom_model: The path to the custom model provided by the user.
|
||||
default_model_path: The path to the default model.
|
||||
|
||||
Returns:
|
||||
The path to the model to be used.
|
||||
"""
|
||||
if custom_model is not None and os.path.exists(custom_model):
|
||||
print(f"Using provided model: {custom_model}")
|
||||
return custom_model
|
||||
else:
|
||||
if custom_model is not None:
|
||||
print(
|
||||
f"Warning: Provided model '{custom_model}' not found. "
|
||||
"Using default model instead."
|
||||
)
|
||||
print(f"Using default model: {default_model_path}")
|
||||
return default_model_path
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright 2023 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Copyright 2023 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""MediaPipe vision benchmarker."""
|
||||
|
||||
import argparse
|
||||
|
||||
from mediapipe.tasks.python.benchmark import benchmark_utils as bu
|
||||
from mediapipe.tasks.python.benchmark.vision.core import base_vision_benchmark_api
|
||||
from mediapipe.tasks.python.core import base_options
|
||||
|
||||
|
||||
def benchmarker(benchmark_function, default_model_name):
|
||||
"""Executes a benchmarking process using a specified function ann model.
|
||||
|
||||
Args:
|
||||
benchmark_function: A callable function to be executed for benchmarking.
|
||||
This function should contain the logic of the task to be benchmarked and
|
||||
should be capable of utilizing a model specified by its name.
|
||||
default_model_name: The name or path of the default model to be used in
|
||||
the benchmarking process. This is useful when the benchmarking function
|
||||
requires a model and no other model is explicitly specified.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--mode',
|
||||
help='Benchmarking mode (e.g., "nth_percentile").',
|
||||
required=False,
|
||||
default='nth_percentile',
|
||||
)
|
||||
parser.add_argument('--model', help='Path to the model.', default=None)
|
||||
parser.add_argument(
|
||||
'--iterations',
|
||||
help='Number of iterations for benchmarking.',
|
||||
type=int,
|
||||
default=100,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--percentile',
|
||||
help='Percentile for benchmarking statistics.',
|
||||
type=float,
|
||||
default=95.0,
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Get the model path
|
||||
default_model_path = bu.get_test_data_path(
|
||||
base_vision_benchmark_api.VISION_TEST_DATA_DIR, default_model_name
|
||||
)
|
||||
model_path = bu.get_model_path(args.model, default_model_path)
|
||||
|
||||
# Define a mapping of modes to their respective function argument lists
|
||||
mode_args_mapping = {
|
||||
'nth_percentile': {'percentile': args.percentile},
|
||||
'average': {},
|
||||
}
|
||||
|
||||
# Check if the mode is supported and get the argument dictionary
|
||||
if args.mode not in mode_args_mapping:
|
||||
raise ValueError(f'Unsupported benchmarking mode: {args.mode}')
|
||||
|
||||
mode_args = mode_args_mapping[args.mode]
|
||||
|
||||
# Run the benchmark for both CPU and GPU and calculate results based on mode
|
||||
results = {}
|
||||
for delegate_type in [
|
||||
base_options.BaseOptions.Delegate.CPU,
|
||||
base_options.BaseOptions.Delegate.GPU,
|
||||
]:
|
||||
inference_times = benchmark_function(
|
||||
model_path, args.iterations, delegate_type
|
||||
)
|
||||
|
||||
# Calculate the benchmark result based on the mode
|
||||
if args.mode == 'nth_percentile':
|
||||
results[delegate_type] = bu.nth_percentile(inference_times, **mode_args)
|
||||
elif args.mode == 'average':
|
||||
results[delegate_type] = bu.average(inference_times)
|
||||
|
||||
# Report benchmarking results
|
||||
for delegate_type, result in results.items():
|
||||
print(
|
||||
f'Inference time {delegate_type} {mode_args_mapping[args.mode]}: '
|
||||
f'{result:.6f} milliseconds'
|
||||
)
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
"""Copyright 2023 The MediaPipe Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# Copyright 2023 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""MediaPipe vision benchmark base api."""
|
||||
import time
|
||||
|
||||
VISION_TEST_DATA_DIR = 'mediapipe/tasks/testdata/vision'
|
||||
|
||||
|
||||
def benchmark_task(func, image, n_iterations):
|
||||
"""Collect inference times for a given task after benchmarking.
|
||||
|
||||
Args:
|
||||
func: The task function used for benchmarking.
|
||||
image: The input MediaPipe Image.
|
||||
n_iterations: Number of iterations to run the benchmark.
|
||||
|
||||
Returns:
|
||||
List of inference times in milliseconds.
|
||||
"""
|
||||
inference_times = []
|
||||
|
||||
for _ in range(n_iterations):
|
||||
start_time_ns = time.time_ns()
|
||||
# Run the method for the task (e.g., classify)
|
||||
func(image)
|
||||
end_time_ns = time.time_ns()
|
||||
inference_times.append((end_time_ns - start_time_ns) / 1_000_000)
|
||||
|
||||
return inference_times
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""MediaPipe Tasks Components Containers API."""
|
||||
|
||||
import mediapipe.tasks.python.components.containers.audio_data
|
||||
import mediapipe.tasks.python.components.containers.bounding_box
|
||||
import mediapipe.tasks.python.components.containers.category
|
||||
import mediapipe.tasks.python.components.containers.classification_result
|
||||
import mediapipe.tasks.python.components.containers.detections
|
||||
import mediapipe.tasks.python.components.containers.embedding_result
|
||||
import mediapipe.tasks.python.components.containers.landmark
|
||||
import mediapipe.tasks.python.components.containers.landmark_detection_result
|
||||
import mediapipe.tasks.python.components.containers.rect
|
||||
|
||||
AudioDataFormat = audio_data.AudioDataFormat
|
||||
AudioData = audio_data.AudioData
|
||||
BoundingBox = bounding_box.BoundingBox
|
||||
Category = category.Category
|
||||
Classifications = classification_result.Classifications
|
||||
ClassificationResult = classification_result.ClassificationResult
|
||||
Detection = detections.Detection
|
||||
DetectionResult = detections.DetectionResult
|
||||
Embedding = embedding_result.Embedding
|
||||
EmbeddingResult = embedding_result.EmbeddingResult
|
||||
Landmark = landmark.Landmark
|
||||
NormalizedLandmark = landmark.NormalizedLandmark
|
||||
LandmarksDetectionResult = landmark_detection_result.LandmarksDetectionResult
|
||||
Rect = rect.Rect
|
||||
NormalizedRect = rect.NormalizedRect
|
||||
|
||||
# Remove unnecessary modules to avoid duplication in API docs.
|
||||
del audio_data
|
||||
del bounding_box
|
||||
del category
|
||||
del classification_result
|
||||
del detections
|
||||
del embedding_result
|
||||
del landmark
|
||||
del landmark_detection_result
|
||||
del rect
|
||||
del mediapipe
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""MediaPipe audio data."""
|
||||
|
||||
import ctypes
|
||||
import dataclasses
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from mediapipe.tasks.python.components.containers import audio_data_c
|
||||
from mediapipe.tasks.python.core.optional_dependencies import doc_controls
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AudioDataFormat:
|
||||
"""Audio format metadata.
|
||||
|
||||
Attributes:
|
||||
num_channels: the number of channels of the audio data.
|
||||
sample_rate: the audio sample rate.
|
||||
"""
|
||||
num_channels: int = 1
|
||||
sample_rate: Optional[float] = None
|
||||
|
||||
|
||||
class AudioData(object):
|
||||
"""MediaPipe Tasks' audio container."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
buffer_length: int,
|
||||
audio_format: AudioDataFormat = AudioDataFormat()
|
||||
) -> None:
|
||||
"""Initializes the `AudioData` object.
|
||||
|
||||
Args:
|
||||
buffer_length: the length of the audio buffer.
|
||||
audio_format: the audio format metadata.
|
||||
"""
|
||||
self._audio_format = audio_format
|
||||
self._buffer = np.zeros([buffer_length, self._audio_format.num_channels],
|
||||
dtype=np.float32)
|
||||
|
||||
def clear(self):
|
||||
"""Clears the internal buffer and fill it with zeros."""
|
||||
self._buffer.fill(0)
|
||||
|
||||
def load_from_array(self,
|
||||
src: np.ndarray,
|
||||
offset: int = 0,
|
||||
size: int = -1) -> None:
|
||||
"""Loads the audio data from a NumPy array.
|
||||
|
||||
Args:
|
||||
src: A NumPy source array contains the input audio.
|
||||
offset: An optional offset for loading a slice of the `src` array to the
|
||||
buffer.
|
||||
size: An optional size parameter denoting the number of samples to load
|
||||
from the `src` array.
|
||||
|
||||
Raises:
|
||||
ValueError: If the input array has an incorrect shape or if
|
||||
`offset` + `size` exceeds the length of the `src` array.
|
||||
"""
|
||||
if len(src.shape) == 1:
|
||||
if self._audio_format.num_channels != 1:
|
||||
raise ValueError(f"Input audio is mono, but the audio data is expected "
|
||||
f"to have {self._audio_format.num_channels} channels.")
|
||||
elif src.shape[1] != self._audio_format.num_channels:
|
||||
raise ValueError(f"Input audio contains an invalid number of channels. "
|
||||
f"Expect {self._audio_format.num_channels}.")
|
||||
|
||||
if size < 0:
|
||||
size = len(src)
|
||||
|
||||
if offset + size > len(src):
|
||||
raise ValueError(
|
||||
f"Index out of range. offset {offset} + size {size} should be <= "
|
||||
f"src's length: {len(src)}")
|
||||
|
||||
if len(src) >= len(self._buffer):
|
||||
# If the internal buffer is shorter than the load target (src), copy
|
||||
# values from the end of the src array to the internal buffer.
|
||||
new_offset = offset + size - len(self._buffer)
|
||||
new_size = len(self._buffer)
|
||||
self._buffer = src[new_offset:new_offset + new_size].copy()
|
||||
else:
|
||||
# Shift the internal buffer backward and add the incoming data to the end
|
||||
# of the buffer.
|
||||
shift = size
|
||||
self._buffer = np.roll(self._buffer, -shift, axis=0)
|
||||
self._buffer[-shift:, :] = src[offset:offset + size].copy()
|
||||
|
||||
@classmethod
|
||||
def create_from_array(cls,
|
||||
src: np.ndarray,
|
||||
sample_rate: Optional[float] = None) -> "AudioData":
|
||||
"""Creates an `AudioData` object from a NumPy array.
|
||||
|
||||
Args:
|
||||
src: A NumPy source array contains the input audio.
|
||||
sample_rate: the optional audio sample rate.
|
||||
|
||||
Returns:
|
||||
An `AudioData` object that contains a copy of the NumPy source array as
|
||||
the data.
|
||||
"""
|
||||
src = src.astype(np.float32)
|
||||
obj = cls(
|
||||
buffer_length=src.shape[0],
|
||||
audio_format=AudioDataFormat(
|
||||
num_channels=1 if len(src.shape) == 1 else src.shape[1],
|
||||
sample_rate=sample_rate))
|
||||
obj.load_from_array(src)
|
||||
return obj
|
||||
|
||||
@property
|
||||
def audio_format(self) -> AudioDataFormat:
|
||||
"""Gets the audio format of the audio."""
|
||||
return self._audio_format
|
||||
|
||||
@property
|
||||
def buffer_length(self) -> int:
|
||||
"""Gets the sample count of the audio."""
|
||||
return self._buffer.shape[0]
|
||||
|
||||
@property
|
||||
def buffer(self) -> np.ndarray:
|
||||
"""Gets the internal buffer."""
|
||||
return self._buffer
|
||||
|
||||
@doc_controls.do_not_generate_docs
|
||||
def to_ctypes(self) -> audio_data_c.AudioDataC:
|
||||
"""Converts the object to a ctypes audio data object."""
|
||||
return audio_data_c.AudioDataC(
|
||||
num_channels=self.audio_format.num_channels,
|
||||
sample_rate=self.audio_format.sample_rate,
|
||||
audio_data=self._buffer.ctypes.data_as(ctypes.POINTER(ctypes.c_float)),
|
||||
audio_data_size=self._buffer.size,
|
||||
)
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# Copyright 2025 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""MediaPipe audio data ctypes."""
|
||||
|
||||
import ctypes
|
||||
|
||||
|
||||
class AudioDataC(ctypes.Structure):
|
||||
"""The audio data used in the C API."""
|
||||
|
||||
_fields_ = [
|
||||
('num_channels', ctypes.c_int),
|
||||
('sample_rate', ctypes.c_double),
|
||||
('audio_data', ctypes.POINTER(ctypes.c_float)),
|
||||
('audio_data_size', ctypes.c_size_t),
|
||||
]
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Bounding box data class."""
|
||||
|
||||
import dataclasses
|
||||
from typing import Any
|
||||
|
||||
from mediapipe.tasks.python.components.containers import rect_c as rect_c_lib
|
||||
from mediapipe.tasks.python.core.optional_dependencies import doc_controls
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class BoundingBox:
|
||||
"""An integer bounding box, axis aligned.
|
||||
|
||||
Attributes:
|
||||
origin_x: The X coordinate of the top-left corner, in pixels.
|
||||
origin_y: The Y coordinate of the top-left corner, in pixels.
|
||||
width: The width of the bounding box, in pixels.
|
||||
height: The height of the bounding box, in pixels.
|
||||
"""
|
||||
|
||||
origin_x: int
|
||||
origin_y: int
|
||||
width: int
|
||||
height: int
|
||||
|
||||
@classmethod
|
||||
@doc_controls.do_not_generate_docs
|
||||
def from_ctypes(cls, c_obj: rect_c_lib.RectC) -> 'BoundingBox':
|
||||
"""Creates a `BoundingBox` object from a ctypes RectC struct."""
|
||||
return BoundingBox(
|
||||
origin_x=c_obj.left,
|
||||
origin_y=c_obj.top,
|
||||
width=c_obj.right - c_obj.left,
|
||||
height=c_obj.bottom - c_obj.top,
|
||||
)
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Checks if this object is equal to the given object.
|
||||
|
||||
Args:
|
||||
other: The object to be compared with.
|
||||
|
||||
Returns:
|
||||
True if the objects are equal.
|
||||
"""
|
||||
if not isinstance(other, BoundingBox):
|
||||
return False
|
||||
return (
|
||||
self.origin_x == other.origin_x
|
||||
and self.origin_y == other.origin_y
|
||||
and self.width == other.width
|
||||
and self.height == other.height
|
||||
)
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Category data class."""
|
||||
|
||||
import dataclasses
|
||||
from typing import Any, Optional
|
||||
|
||||
from mediapipe.tasks.python.components.containers import category_c as category_c_module
|
||||
from mediapipe.tasks.python.core.optional_dependencies import doc_controls
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Category:
|
||||
"""A classification category.
|
||||
|
||||
Category is a util class, contains a label, its display name, a float
|
||||
value as score, and the index of the label in the corresponding label file.
|
||||
Typically it's used as the result of classification tasks.
|
||||
|
||||
Attributes:
|
||||
index: The index of the label in the corresponding label file.
|
||||
score: The probability score of this label category.
|
||||
display_name: The display name of the label, which may be translated for
|
||||
different locales. For example, a label, "apple", may be translated into
|
||||
Spanish for display purpose, so that the `display_name` is "manzana".
|
||||
category_name: The label of this category object.
|
||||
"""
|
||||
|
||||
index: Optional[int] = None
|
||||
score: Optional[float] = None
|
||||
display_name: Optional[str] = None
|
||||
category_name: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
@doc_controls.do_not_generate_docs
|
||||
def from_ctypes(cls, c_obj: category_c_module.CategoryC) -> "Category":
|
||||
"""Creates a `Category` object from the given `CategoryC` object.
|
||||
|
||||
This function converts the `CategoryC` index of -1 to a Python value of None
|
||||
to retain the same semantic meaning. All other values are converted as-is.
|
||||
|
||||
Args:
|
||||
c_obj: The `CategoryC` object to be converted.
|
||||
|
||||
Returns:
|
||||
A `Category` object.
|
||||
"""
|
||||
return Category(
|
||||
index=c_obj.index if c_obj.index != -1 else None,
|
||||
score=c_obj.score,
|
||||
category_name=(
|
||||
c_obj.category_name.decode("utf-8") if c_obj.category_name else None
|
||||
),
|
||||
display_name=(
|
||||
c_obj.display_name.decode("utf-8") if c_obj.display_name else None
|
||||
),
|
||||
)
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Checks if this object is equal to the given object.
|
||||
|
||||
Args:
|
||||
other: The object to be compared with.
|
||||
|
||||
Returns:
|
||||
True if the objects are equal.
|
||||
"""
|
||||
if not isinstance(other, Category):
|
||||
return False
|
||||
return (
|
||||
self.index == other.index
|
||||
and self.score == other.score
|
||||
and self.display_name == other.display_name
|
||||
and self.category_name == other.category_name
|
||||
)
|
||||
|
||||
|
||||
@doc_controls.do_not_generate_docs
|
||||
def create_list_of_categories_from_ctypes(
|
||||
c_obj: category_c_module.CategoriesC,
|
||||
) -> list[Category]:
|
||||
"""Creates a list of `Category` objects from a `CategoriesC` object."""
|
||||
return [
|
||||
Category.from_ctypes(c_obj.categories[i])
|
||||
for i in range(c_obj.categories_count)
|
||||
]
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Copyright 2025 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""C types for Category."""
|
||||
|
||||
import ctypes
|
||||
|
||||
|
||||
class CategoryC(ctypes.Structure):
|
||||
"""The ctypes struct for Category."""
|
||||
|
||||
_fields_ = [
|
||||
("index", ctypes.c_int),
|
||||
("score", ctypes.c_float),
|
||||
("category_name", ctypes.c_char_p),
|
||||
("display_name", ctypes.c_char_p),
|
||||
]
|
||||
|
||||
|
||||
class CategoriesC(ctypes.Structure):
|
||||
"""C types for a list of Categories."""
|
||||
|
||||
_fields_ = [
|
||||
("categories", ctypes.POINTER(CategoryC)),
|
||||
("categories_count", ctypes.c_uint32),
|
||||
]
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
# Copyright 2022 The TensorFlow Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Classifications data class."""
|
||||
|
||||
import dataclasses
|
||||
from typing import List, Optional
|
||||
|
||||
from mediapipe.tasks.python.components.containers import category as category_lib
|
||||
from mediapipe.tasks.python.components.containers import classification_result_c
|
||||
from mediapipe.tasks.python.core.optional_dependencies import doc_controls
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Classifications:
|
||||
"""Represents the classification results for a given classifier head.
|
||||
|
||||
Attributes:
|
||||
categories: The array of predicted categories, usually sorted by descending
|
||||
scores (e.g. from high to low probability).
|
||||
head_index: The index of the classifier head these categories refer to. This
|
||||
is useful for multi-head models.
|
||||
head_name: The name of the classifier head, which is the corresponding
|
||||
tensor metadata name.
|
||||
"""
|
||||
|
||||
categories: List[category_lib.Category]
|
||||
head_index: int
|
||||
head_name: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
@doc_controls.do_not_generate_docs
|
||||
def from_ctypes(
|
||||
cls, c_struct: classification_result_c.ClassificationsC
|
||||
) -> 'Classifications':
|
||||
"""Creates a `Classifications` object from the given ctypes struct."""
|
||||
if c_struct.categories and c_struct.categories_count > 0:
|
||||
categories = [
|
||||
category_lib.Category.from_ctypes(c_struct.categories[i])
|
||||
for i in range(c_struct.categories_count)
|
||||
]
|
||||
else:
|
||||
categories = []
|
||||
|
||||
return Classifications(
|
||||
categories=categories,
|
||||
head_index=c_struct.head_index,
|
||||
head_name=(
|
||||
c_struct.head_name.decode('utf-8') if c_struct.head_name else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ClassificationResult:
|
||||
"""Contains the classification results of a model.
|
||||
|
||||
Attributes:
|
||||
classifications: A list of `Classifications` objects, each for a head of the
|
||||
model.
|
||||
timestamp_ms: The optional timestamp (in milliseconds) of the start of the
|
||||
chunk of data corresponding to these results. This is only used for
|
||||
classification on time series (e.g. audio classification). In these use
|
||||
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.
|
||||
"""
|
||||
|
||||
classifications: List[Classifications]
|
||||
timestamp_ms: Optional[int] = None
|
||||
|
||||
@classmethod
|
||||
@doc_controls.do_not_generate_docs
|
||||
def from_ctypes(
|
||||
cls, c_struct: classification_result_c.ClassificationResultC
|
||||
) -> 'ClassificationResult':
|
||||
"""Creates a `ClassificationResult` object from a ctypes struct."""
|
||||
if c_struct.classifications and c_struct.classifications_count > 0:
|
||||
classifications = [
|
||||
Classifications.from_ctypes(c_struct.classifications[i])
|
||||
for i in range(c_struct.classifications_count)
|
||||
]
|
||||
else:
|
||||
classifications = []
|
||||
|
||||
timestamp_ms = c_struct.timestamp_ms if c_struct.has_timestamp_ms else None
|
||||
return ClassificationResult(
|
||||
classifications=classifications, timestamp_ms=timestamp_ms
|
||||
)
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Copyright 2025 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""C types for ClassificationResult."""
|
||||
|
||||
import ctypes
|
||||
|
||||
from mediapipe.tasks.python.components.containers import category_c
|
||||
|
||||
|
||||
class ClassificationsC(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("categories", ctypes.POINTER(category_c.CategoryC)),
|
||||
("categories_count", ctypes.c_uint32),
|
||||
("head_index", ctypes.c_int),
|
||||
("head_name", ctypes.c_char_p),
|
||||
]
|
||||
|
||||
|
||||
class ClassificationResultC(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("classifications", ctypes.POINTER(ClassificationsC)),
|
||||
("classifications_count", ctypes.c_uint32),
|
||||
("timestamp_ms", ctypes.c_int64),
|
||||
("has_timestamp_ms", ctypes.c_bool),
|
||||
]
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Detections data class."""
|
||||
|
||||
import dataclasses
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from mediapipe.tasks.python.components.containers import bounding_box as bounding_box_lib
|
||||
from mediapipe.tasks.python.components.containers import category as category_lib
|
||||
from mediapipe.tasks.python.components.containers import category_c as category_c_lib
|
||||
from mediapipe.tasks.python.components.containers import detections_c as detections_c_lib
|
||||
from mediapipe.tasks.python.components.containers import keypoint as keypoint_lib
|
||||
from mediapipe.tasks.python.core.optional_dependencies import doc_controls
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Detection:
|
||||
"""Represents one detected object in the object detector's results.
|
||||
|
||||
Attributes:
|
||||
bounding_box: A BoundingBox object.
|
||||
categories: A list of Category objects.
|
||||
keypoints: A list of NormalizedKeypoint objects.
|
||||
"""
|
||||
|
||||
bounding_box: bounding_box_lib.BoundingBox
|
||||
categories: List[category_lib.Category]
|
||||
keypoints: Optional[List[keypoint_lib.NormalizedKeypoint]] = None
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Checks if this object is equal to the given object.
|
||||
|
||||
Args:
|
||||
other: The object to be compared with.
|
||||
|
||||
Returns:
|
||||
True if the objects are equal.
|
||||
"""
|
||||
if not isinstance(other, Detection):
|
||||
return False
|
||||
return (
|
||||
self.bounding_box == other.bounding_box
|
||||
and self.categories == other.categories
|
||||
and self.keypoints == other.keypoints
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@doc_controls.do_not_generate_docs
|
||||
def from_ctypes(cls, c_obj: detections_c_lib.DetectionC) -> 'Detection':
|
||||
"""Creates a `Detection` object from the given `DetectionC` object."""
|
||||
c_categories = category_c_lib.CategoriesC(
|
||||
categories=c_obj.categories, categories_count=c_obj.categories_count
|
||||
)
|
||||
|
||||
py_categories = category_lib.create_list_of_categories_from_ctypes(
|
||||
c_categories
|
||||
)
|
||||
py_bounding_box = bounding_box_lib.BoundingBox.from_ctypes(
|
||||
c_obj.bounding_box
|
||||
)
|
||||
|
||||
if c_obj.keypoints and c_obj.keypoints_count > 0:
|
||||
py_keypoints = [
|
||||
keypoint_lib.NormalizedKeypoint.from_ctypes(c_obj.keypoints[i])
|
||||
for i in range(c_obj.keypoints_count)
|
||||
]
|
||||
else:
|
||||
py_keypoints = None
|
||||
|
||||
return Detection(
|
||||
bounding_box=py_bounding_box,
|
||||
categories=py_categories,
|
||||
keypoints=py_keypoints,
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class DetectionResult:
|
||||
"""Represents the list of detected objects.
|
||||
|
||||
Attributes:
|
||||
detections: A list of `Detection` objects.
|
||||
"""
|
||||
|
||||
detections: List[Detection]
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Checks if this object is equal to the given object.
|
||||
|
||||
Args:
|
||||
other: The object to be compared with.
|
||||
|
||||
Returns:
|
||||
True if the objects are equal.
|
||||
"""
|
||||
if not isinstance(other, DetectionResult):
|
||||
return False
|
||||
return self.detections == other.detections
|
||||
|
||||
@classmethod
|
||||
@doc_controls.do_not_generate_docs
|
||||
def from_ctypes(
|
||||
cls, c_obj: detections_c_lib.DetectionResultC
|
||||
) -> 'DetectionResult':
|
||||
"""Creates a `DetectionResult` object from a `DetectionResultC` object."""
|
||||
return DetectionResult(
|
||||
detections=[
|
||||
Detection.from_ctypes(c_obj.detections[i])
|
||||
for i in range(c_obj.detections_count)
|
||||
]
|
||||
)
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# Copyright 2025 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""MediaPipe Detection Result C API types."""
|
||||
|
||||
import ctypes
|
||||
|
||||
from mediapipe.tasks.python.components.containers import category_c
|
||||
from mediapipe.tasks.python.components.containers import keypoint_c
|
||||
from mediapipe.tasks.python.components.containers import rect_c
|
||||
|
||||
|
||||
class DetectionC(ctypes.Structure):
|
||||
"""CTypes for a single detection."""
|
||||
|
||||
_fields_ = [
|
||||
('categories', ctypes.POINTER(category_c.CategoryC)),
|
||||
('categories_count', ctypes.c_uint32),
|
||||
('bounding_box', rect_c.RectC),
|
||||
('keypoints', ctypes.POINTER(keypoint_c.NormalizedKeypointC)),
|
||||
('keypoints_count', ctypes.c_uint32),
|
||||
]
|
||||
|
||||
|
||||
class DetectionResultC(ctypes.Structure):
|
||||
"""CTypes for the detection result."""
|
||||
|
||||
_fields_ = [
|
||||
('detections', ctypes.POINTER(DetectionC)),
|
||||
('detections_count', ctypes.c_uint32),
|
||||
]
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Embeddings data class."""
|
||||
|
||||
import dataclasses
|
||||
from typing import List, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from mediapipe.tasks.python.components.containers import embedding_result_c
|
||||
from mediapipe.tasks.python.core.optional_dependencies import doc_controls
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Embedding:
|
||||
"""Embedding result for a given embedder head.
|
||||
|
||||
Attributes:
|
||||
embedding: The actual embedding, either floating-point or scalar-quantized.
|
||||
head_index: The index of the embedder head that produced this embedding.
|
||||
This is useful for multi-head models.
|
||||
head_name: The name of the embedder head, which is the corresponding tensor
|
||||
metadata name (if any). This is useful for multi-head models.
|
||||
"""
|
||||
|
||||
embedding: np.ndarray
|
||||
head_index: Optional[int] = None
|
||||
head_name: Optional[str] = None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class EmbeddingResult:
|
||||
"""Embedding results for a given embedder model.
|
||||
|
||||
Attributes:
|
||||
embeddings: A list of `Embedding` objects.
|
||||
timestamp_ms: The optional timestamp (in milliseconds) of the start of the
|
||||
chunk of data corresponding to these results. This is only used for
|
||||
embedding extraction on time series (e.g. audio embedding). In these use
|
||||
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.
|
||||
"""
|
||||
|
||||
embeddings: List[Embedding]
|
||||
timestamp_ms: Optional[int] = None
|
||||
|
||||
@classmethod
|
||||
@doc_controls.do_not_generate_docs
|
||||
def from_ctypes(
|
||||
cls, c_result: embedding_result_c.EmbeddingResultC
|
||||
) -> 'EmbeddingResult':
|
||||
"""Converts a C EmbeddingResult to a Python EmbeddingResult."""
|
||||
python_embeddings = []
|
||||
for i in range(c_result.embeddings_count):
|
||||
c_embedding = c_result.embeddings[i]
|
||||
embedding = None
|
||||
if c_embedding.float_embedding:
|
||||
embedding_array = list(
|
||||
c_embedding.float_embedding[: c_embedding.values_count]
|
||||
)
|
||||
embedding = np.array(embedding_array, dtype=float)
|
||||
if c_embedding.quantized_embedding:
|
||||
embedding_array = list(
|
||||
c_embedding.quantized_embedding[: c_embedding.values_count]
|
||||
)
|
||||
embedding = np.array(embedding_array, dtype=np.uint8)
|
||||
|
||||
python_embedding = Embedding(
|
||||
embedding=embedding,
|
||||
head_index=c_embedding.head_index,
|
||||
head_name=(
|
||||
c_embedding.head_name.decode('utf-8')
|
||||
if c_embedding.head_name
|
||||
else None
|
||||
),
|
||||
)
|
||||
python_embeddings.append(python_embedding)
|
||||
timestamp_ms = c_result.timestamp_ms if c_result.has_timestamp_ms else None
|
||||
return EmbeddingResult(
|
||||
embeddings=python_embeddings, timestamp_ms=timestamp_ms
|
||||
)
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Embeddings C API types."""
|
||||
|
||||
import ctypes
|
||||
|
||||
|
||||
class EmbeddingC(ctypes.Structure):
|
||||
"""C struct for an embedding."""
|
||||
|
||||
_fields_ = [
|
||||
('float_embedding', ctypes.POINTER(ctypes.c_float)),
|
||||
('quantized_embedding', ctypes.POINTER(ctypes.c_uint8)),
|
||||
('values_count', ctypes.c_uint32),
|
||||
('head_index', ctypes.c_int),
|
||||
('head_name', ctypes.c_char_p),
|
||||
]
|
||||
|
||||
|
||||
class EmbeddingResultC(ctypes.Structure):
|
||||
"""C struct for embedding result."""
|
||||
|
||||
_fields_ = [
|
||||
('embeddings', ctypes.POINTER(EmbeddingC)),
|
||||
('embeddings_count', ctypes.c_uint32),
|
||||
('has_timestamp_ms', ctypes.c_bool),
|
||||
('timestamp_ms', ctypes.c_int64),
|
||||
]
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
# Copyright 2023 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Keypoint data class."""
|
||||
|
||||
import dataclasses
|
||||
from typing import Any, Optional
|
||||
|
||||
from mediapipe.tasks.python.components.containers import keypoint_c as keypoint_c_lib
|
||||
from mediapipe.tasks.python.core.optional_dependencies import doc_controls
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class NormalizedKeypoint:
|
||||
"""A normalized keypoint.
|
||||
|
||||
Normalized keypoint represents a point in 2D space with x, y coordinates.
|
||||
x and y are normalized to [0.0, 1.0] by the image width and height
|
||||
respectively.
|
||||
|
||||
Attributes:
|
||||
x: The x coordinates of the normalized keypoint.
|
||||
y: The y coordinates of the normalized keypoint.
|
||||
label: The optional label of the keypoint.
|
||||
score: The score of the keypoint.
|
||||
"""
|
||||
|
||||
x: Optional[float] = None
|
||||
y: Optional[float] = None
|
||||
label: Optional[str] = None
|
||||
score: Optional[float] = None
|
||||
|
||||
@classmethod
|
||||
@doc_controls.do_not_generate_docs
|
||||
def from_ctypes(
|
||||
cls, c_obj: keypoint_c_lib.NormalizedKeypointC
|
||||
) -> 'NormalizedKeypoint':
|
||||
"""Creates a `NormalizedKeypoint` object from a `NormalizedKeypointC."""
|
||||
return NormalizedKeypoint(
|
||||
x=c_obj.x,
|
||||
y=c_obj.y,
|
||||
label=c_obj.label.decode('utf-8') if c_obj.label else None,
|
||||
score=c_obj.score,
|
||||
)
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Checks if this object is equal to the given object.
|
||||
|
||||
Args:
|
||||
other: The object to be compared with.
|
||||
|
||||
Returns:
|
||||
True if the objects are equal.
|
||||
"""
|
||||
if not isinstance(other, NormalizedKeypoint):
|
||||
return False
|
||||
return (
|
||||
self.x == other.x
|
||||
and self.y == other.y
|
||||
and self.label == other.label
|
||||
and self.score == other.score
|
||||
)
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# Copyright 2025 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""MediaPipe Keypoint C API types."""
|
||||
|
||||
import ctypes
|
||||
|
||||
|
||||
class NormalizedKeypointC(ctypes.Structure):
|
||||
"""A keypoint in normalized coordinates."""
|
||||
|
||||
_fields_ = [
|
||||
('x', ctypes.c_float),
|
||||
('y', ctypes.c_float),
|
||||
('label', ctypes.c_char_p),
|
||||
('score', ctypes.c_float),
|
||||
]
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Landmark data class."""
|
||||
|
||||
import dataclasses
|
||||
from typing import Optional
|
||||
|
||||
from mediapipe.tasks.python.components.containers import landmark_c as landmark_c_module
|
||||
from mediapipe.tasks.python.core.optional_dependencies import doc_controls
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Landmark:
|
||||
"""A landmark that can have 1 to 3 dimensions.
|
||||
|
||||
Use x for 1D points, (x, y) for 2D points and (x, y, z) for 3D points.
|
||||
|
||||
Attributes:
|
||||
x: The x coordinate.
|
||||
y: The y coordinate.
|
||||
z: The z coordinate.
|
||||
visibility: Landmark visibility. Should stay unset if not supported. Float
|
||||
score of whether landmark is visible or occluded by other objects.
|
||||
Landmark considered as invisible also if it is not present on the screen
|
||||
(out of scene bounds). Depending on the model, visibility value is either
|
||||
a sigmoid or an argument of sigmoid.
|
||||
presence: Landmark presence. Should stay unset if not supported. Float score
|
||||
of whether landmark is present on the scene (located within scene bounds).
|
||||
Depending on the model, presence value is either a result of sigmoid or an
|
||||
argument of sigmoid function to get landmark presence probability.
|
||||
name: The name of the landmark.
|
||||
"""
|
||||
|
||||
x: Optional[float] = None
|
||||
y: Optional[float] = None
|
||||
z: Optional[float] = None
|
||||
visibility: Optional[float] = None
|
||||
presence: Optional[float] = None
|
||||
name: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
@doc_controls.do_not_generate_docs
|
||||
def from_ctypes(cls, c_struct: landmark_c_module.LandmarkC) -> 'Landmark':
|
||||
"""Creates a `Landmark` object from the given ctypes struct."""
|
||||
return Landmark(
|
||||
x=c_struct.x,
|
||||
y=c_struct.y,
|
||||
z=c_struct.z,
|
||||
visibility=c_struct.visibility if c_struct.has_visibility else None,
|
||||
presence=c_struct.presence if c_struct.has_presence else None,
|
||||
name=c_struct.name.decode('utf-8') if c_struct.name else None,
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class NormalizedLandmark:
|
||||
"""A normalized version of above Landmark proto.
|
||||
|
||||
All coordinates should be within [0, 1].
|
||||
|
||||
Attributes:
|
||||
x: The normalized x coordinate.
|
||||
y: The normalized y coordinate.
|
||||
z: The normalized z coordinate.
|
||||
visibility: Landmark visibility. Should stay unset if not supported. Float
|
||||
score of whether landmark is visible or occluded by other objects.
|
||||
Landmark considered as invisible also if it is not present on the screen
|
||||
(out of scene bounds). Depending on the model, visibility value is either
|
||||
a sigmoid or an argument of sigmoid.
|
||||
presence: Landmark presence. Should stay unset if not supported. Float score
|
||||
of whether landmark is present on the scene (located within scene bounds).
|
||||
Depending on the model, presence value is either a result of sigmoid or an
|
||||
argument of sigmoid function to get landmark presence probability.
|
||||
name: The name of the landmark.
|
||||
"""
|
||||
|
||||
x: Optional[float] = None
|
||||
y: Optional[float] = None
|
||||
z: Optional[float] = None
|
||||
visibility: Optional[float] = None
|
||||
presence: Optional[float] = None
|
||||
name: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
@doc_controls.do_not_generate_docs
|
||||
def from_ctypes(
|
||||
cls, c_struct: landmark_c_module.NormalizedLandmarkC
|
||||
) -> 'NormalizedLandmark':
|
||||
"""Creates a `NormalizedLandmark` object from the given ctypes struct."""
|
||||
return NormalizedLandmark(
|
||||
x=c_struct.x,
|
||||
y=c_struct.y,
|
||||
z=c_struct.z,
|
||||
visibility=c_struct.visibility if c_struct.has_visibility else None,
|
||||
presence=c_struct.presence if c_struct.has_presence else None,
|
||||
name=c_struct.name.decode('utf-8') if c_struct.name else None,
|
||||
)
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
# Copyright 2025 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""C types for Landmark."""
|
||||
|
||||
import ctypes
|
||||
|
||||
|
||||
class LandmarkC(ctypes.Structure):
|
||||
"""The ctypes struct for Landmark."""
|
||||
|
||||
_fields_ = [
|
||||
('x', ctypes.c_float),
|
||||
('y', ctypes.c_float),
|
||||
('z', ctypes.c_float),
|
||||
('has_visibility', ctypes.c_bool),
|
||||
('visibility', ctypes.c_float),
|
||||
('has_presence', ctypes.c_bool),
|
||||
('presence', ctypes.c_float),
|
||||
('name', ctypes.c_char_p),
|
||||
]
|
||||
|
||||
|
||||
class LandmarksC(ctypes.Structure):
|
||||
"""The ctypes struct for a list of Landmarks."""
|
||||
|
||||
_fields_ = [
|
||||
('landmarks', ctypes.POINTER(LandmarkC)),
|
||||
('landmarks_count', ctypes.c_uint32),
|
||||
]
|
||||
|
||||
|
||||
class NormalizedLandmarkC(ctypes.Structure):
|
||||
"""The ctypes struct for NormalizedLandmark."""
|
||||
|
||||
_fields_ = [
|
||||
('x', ctypes.c_float),
|
||||
('y', ctypes.c_float),
|
||||
('z', ctypes.c_float),
|
||||
('has_visibility', ctypes.c_bool),
|
||||
('visibility', ctypes.c_float),
|
||||
('has_presence', ctypes.c_bool),
|
||||
('presence', ctypes.c_float),
|
||||
('name', ctypes.c_char_p),
|
||||
]
|
||||
|
||||
|
||||
class NormalizedLandmarksC(ctypes.Structure):
|
||||
"""The ctypes struct for a list of NormalizedLandmarks."""
|
||||
|
||||
_fields_ = [
|
||||
('landmarks', ctypes.POINTER(NormalizedLandmarkC)),
|
||||
('landmarks_count', ctypes.c_uint32),
|
||||
]
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Landmarks Detection Result data class."""
|
||||
|
||||
import dataclasses
|
||||
from typing import Optional, List
|
||||
|
||||
from mediapipe.tasks.python.components.containers import category as category_module
|
||||
from mediapipe.tasks.python.components.containers import landmark as landmark_module
|
||||
from mediapipe.tasks.python.components.containers import rect as rect_module
|
||||
|
||||
_Category = category_module.Category
|
||||
_NormalizedLandmark = landmark_module.NormalizedLandmark
|
||||
_Landmark = landmark_module.Landmark
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class LandmarksDetectionResult:
|
||||
"""Represents the landmarks detection result.
|
||||
|
||||
Attributes:
|
||||
landmarks: A list of `NormalizedLandmark` objects.
|
||||
categories: A list of `Category` objects.
|
||||
world_landmarks: A list of `Landmark` objects.
|
||||
rect: A `NormalizedRect` object.
|
||||
"""
|
||||
|
||||
landmarks: Optional[List[_NormalizedLandmark]]
|
||||
categories: Optional[List[_Category]]
|
||||
world_landmarks: Optional[List[_Landmark]]
|
||||
rect: rect_module.NormalizedRect
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
# Copyright 2025 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""C types for Matrix."""
|
||||
|
||||
import ctypes
|
||||
|
||||
import numpy as np
|
||||
|
||||
from mediapipe.tasks.python.core.optional_dependencies import doc_controls
|
||||
|
||||
|
||||
class MatrixC(ctypes.Structure):
|
||||
"""The ctypes struct for Matrix."""
|
||||
|
||||
_fields_ = [
|
||||
('rows', ctypes.c_uint32),
|
||||
('cols', ctypes.c_uint32),
|
||||
('data', ctypes.POINTER(ctypes.c_float)),
|
||||
]
|
||||
|
||||
@doc_controls.do_not_generate_docs
|
||||
def to_numpy(self) -> np.ndarray:
|
||||
"""Copies the MatrixC struct to a numpy array.
|
||||
|
||||
The data is reshaped from column-major order to (cols, rows) and then
|
||||
transposed to a row-major numpy array. The underlying data is copied to
|
||||
ensure the data is not freed when the underlying C Matrix is closed.
|
||||
|
||||
Returns:
|
||||
A numpy array representing the MatrixC struct.
|
||||
"""
|
||||
if not self.data or self.rows == 0 or self.cols == 0:
|
||||
return np.empty((0,))
|
||||
|
||||
np_array = np.ctypeslib.as_array(
|
||||
self.data, shape=(self.cols, self.rows)
|
||||
).copy()
|
||||
return np_array.T
|
||||
@@ -0,0 +1,123 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Rect data class."""
|
||||
|
||||
import dataclasses
|
||||
from typing import Any, Optional
|
||||
|
||||
from mediapipe.tasks.python.components.containers import rect_c as rect_c_module
|
||||
from mediapipe.tasks.python.core.optional_dependencies import doc_controls
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Rect:
|
||||
"""A rectangle, used as part of detection results.
|
||||
|
||||
Absolute coordinates. The origin is on the top-left corner of the image.
|
||||
|
||||
Attributes:
|
||||
left: The X coordinate of the left side of the rectangle.
|
||||
top: The Y coordinate of the top of the rectangle.
|
||||
bottom: The Y coordinate of the bottom of the rectangle.
|
||||
right: The X coordinate of the right side of the rectangle.
|
||||
"""
|
||||
|
||||
left: int
|
||||
top: int
|
||||
bottom: int
|
||||
right: int
|
||||
|
||||
@doc_controls.do_not_generate_docs
|
||||
def to_ctypes(self) -> rect_c_module.RectC:
|
||||
"""Generates a C API RectC object."""
|
||||
return rect_c_module.RectC(
|
||||
left=self.left, top=self.top, bottom=self.bottom, right=self.right
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class RectF:
|
||||
"""A rectangle, used as part as input region-of-interest.
|
||||
|
||||
The coordinates are normalized wrt the image dimensions, i.e. generally in
|
||||
[0,1] but they may exceed these bounds if describing a region overlapping the
|
||||
image. The origin is on the top-left corner of the image.
|
||||
|
||||
Attributes:
|
||||
left: The X coordinate of the left side of the rectangle.
|
||||
top: The Y coordinate of the top of the rectangle.
|
||||
bottom: The Y coordinate of the bottom of the rectangle.
|
||||
right: The X coordinate of the right side of the rectangle.
|
||||
"""
|
||||
|
||||
left: float
|
||||
top: float
|
||||
bottom: float
|
||||
right: float
|
||||
|
||||
@doc_controls.do_not_generate_docs
|
||||
def to_ctypes(self) -> rect_c_module.RectFC:
|
||||
"""Generates a C API RectFC object."""
|
||||
return rect_c_module.RectFC(
|
||||
left=self.left, top=self.top, bottom=self.bottom, right=self.right
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class NormalizedRect:
|
||||
"""A rectangle with rotation in normalized coordinates.
|
||||
|
||||
Location of the center of the rectangle in image coordinates. The (0.0, 0.0)
|
||||
point is at the (top, left) corner.
|
||||
|
||||
The values of box center location and size are within [0, 1].
|
||||
|
||||
Attributes:
|
||||
x_center: The normalized X coordinate of the rectangle, in image
|
||||
coordinates.
|
||||
y_center: The normalized Y coordinate of the rectangle, in image
|
||||
coordinates.
|
||||
width: The width of the rectangle.
|
||||
height: The height of the rectangle.
|
||||
rotation: Rotation angle is clockwise in radians.
|
||||
rect_id: Optional unique id to help associate different rectangles to each
|
||||
other.
|
||||
"""
|
||||
|
||||
x_center: float
|
||||
y_center: float
|
||||
width: float
|
||||
height: float
|
||||
rotation: Optional[float] = 0.0
|
||||
rect_id: Optional[int] = None
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Checks if this object is equal to the given object.
|
||||
|
||||
Args:
|
||||
other: The object to be compared with.
|
||||
|
||||
Returns:
|
||||
True if the objects are equal.
|
||||
"""
|
||||
if not isinstance(other, NormalizedRect):
|
||||
return False
|
||||
return (
|
||||
self.x_center == other.x_center
|
||||
and self.y_center == other.y_center
|
||||
and self.width == other.width
|
||||
and self.height == other.height
|
||||
and self.rotation == other.rotation
|
||||
and self.rect_id == other.rect_id
|
||||
)
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
# Copyright 2025 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""MediaPipe Rect C API types."""
|
||||
|
||||
|
||||
import ctypes
|
||||
|
||||
|
||||
class RectC(ctypes.Structure):
|
||||
"""A rectangle, used as part of detection results."""
|
||||
|
||||
_fields_ = [
|
||||
('left', ctypes.c_int),
|
||||
('top', ctypes.c_int),
|
||||
('bottom', ctypes.c_int),
|
||||
('right', ctypes.c_int),
|
||||
]
|
||||
|
||||
|
||||
class RectFC(ctypes.Structure):
|
||||
"""A rectangle (operating on floats), used as part of processing options."""
|
||||
|
||||
_fields_ = [
|
||||
('left', ctypes.c_float),
|
||||
('top', ctypes.c_float),
|
||||
('bottom', ctypes.c_float),
|
||||
('right', ctypes.c_float),
|
||||
]
|
||||
|
||||
|
||||
class NormalizedRectC(ctypes.Structure):
|
||||
"""A rectangle with rotation in normalized coordinates."""
|
||||
|
||||
_fields_ = [
|
||||
('left', ctypes.c_float),
|
||||
('top', ctypes.c_float),
|
||||
('bottom', ctypes.c_float),
|
||||
('right', ctypes.c_float),
|
||||
]
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""MediaPipe Tasks Components Processors API."""
|
||||
|
||||
import mediapipe.tasks.python.components.processors.classifier_options
|
||||
|
||||
ClassifierOptions = classifier_options.ClassifierOptions
|
||||
|
||||
# Remove unnecessary modules to avoid duplication in API docs.
|
||||
del classifier_options
|
||||
del mediapipe
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
# Copyright 2022 The TensorFlow Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Classifier options data class."""
|
||||
|
||||
import dataclasses
|
||||
from typing import Any, List, Optional
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ClassifierOptions:
|
||||
"""Options for classification processor.
|
||||
|
||||
Attributes:
|
||||
display_names_locale: The locale to use for display names specified through
|
||||
the TFLite Model Metadata.
|
||||
max_results: The maximum number of top-scored classification results to
|
||||
return.
|
||||
score_threshold: Overrides the ones provided in the model metadata. Results
|
||||
below this value are rejected.
|
||||
category_allowlist: Allowlist of category names. If non-empty, detection
|
||||
results whose category name is not in this set will be filtered out.
|
||||
Duplicate or unknown category names are ignored. Mutually exclusive with
|
||||
`category_denylist`.
|
||||
category_denylist: Denylist of category names. If non-empty, detection
|
||||
results whose category name is in this set will be filtered out. Duplicate
|
||||
or unknown category names are ignored. Mutually exclusive with
|
||||
`category_allowlist`.
|
||||
"""
|
||||
|
||||
display_names_locale: Optional[str] = None
|
||||
max_results: Optional[int] = None
|
||||
score_threshold: Optional[float] = None
|
||||
category_allowlist: Optional[List[str]] = None
|
||||
category_denylist: Optional[List[str]] = None
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Checks if this object is equal to the given object.
|
||||
|
||||
Args:
|
||||
other: The object to be compared with.
|
||||
|
||||
Returns:
|
||||
True if the objects are equal.
|
||||
"""
|
||||
if not isinstance(other, ClassifierOptions):
|
||||
return False
|
||||
return (
|
||||
self.display_names_locale == other.display_names_locale
|
||||
and self.max_results == other.max_results
|
||||
and self.score_threshold == other.score_threshold
|
||||
and self.category_allowlist == other.category_allowlist
|
||||
and self.category_denylist == other.category_denylist
|
||||
)
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
# Copyright 2025 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""C types for ClassifierOptions."""
|
||||
|
||||
import ctypes
|
||||
|
||||
import mediapipe.tasks.python.components.processors.classifier_options as classifier_options_module
|
||||
|
||||
|
||||
class ClassifierOptionsC(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("display_names_locale", ctypes.c_char_p),
|
||||
("max_results", ctypes.c_int),
|
||||
("score_threshold", ctypes.c_float),
|
||||
("category_allowlist", ctypes.POINTER(ctypes.c_char_p)),
|
||||
("category_allowlist_count", ctypes.c_uint32),
|
||||
("category_denylist", ctypes.POINTER(ctypes.c_char_p)),
|
||||
("category_denylist_count", ctypes.c_uint32),
|
||||
]
|
||||
|
||||
|
||||
def convert_to_classifier_options_c(
|
||||
src: classifier_options_module.ClassifierOptions,
|
||||
) -> ClassifierOptionsC:
|
||||
"""Converts a Python ClassifierOptions object to a ClassifierOptionsC object."""
|
||||
options = ClassifierOptionsC()
|
||||
|
||||
if src.display_names_locale is not None:
|
||||
options.display_names_locale = src.display_names_locale.encode("utf-8")
|
||||
else:
|
||||
options.display_names_locale = None
|
||||
|
||||
if src.max_results is not None:
|
||||
options.max_results = src.max_results
|
||||
else:
|
||||
options.max_results = -1
|
||||
|
||||
if src.score_threshold is not None:
|
||||
options.score_threshold = src.score_threshold
|
||||
else:
|
||||
options.score_threshold = 0.0
|
||||
|
||||
# Handle category_allowlist
|
||||
if src.category_allowlist:
|
||||
bytes_allowlist = [s.encode("utf-8") for s in src.category_allowlist]
|
||||
allowlist_array_type = ctypes.c_char_p * len(bytes_allowlist)
|
||||
allowlist_array = allowlist_array_type(*bytes_allowlist)
|
||||
options.category_allowlist = ctypes.cast(
|
||||
allowlist_array, ctypes.POINTER(ctypes.c_char_p)
|
||||
)
|
||||
options.category_allowlist_count = len(bytes_allowlist)
|
||||
else:
|
||||
options.category_allowlist = None
|
||||
options.category_allowlist_count = 0
|
||||
|
||||
# Handle category_denylist
|
||||
if src.category_denylist:
|
||||
bytes_denylist = [s.encode("utf-8") for s in src.category_denylist]
|
||||
denylist_array_type = ctypes.c_char_p * len(bytes_denylist)
|
||||
denylist_array = denylist_array_type(*bytes_denylist)
|
||||
options.category_denylist = ctypes.cast(
|
||||
denylist_array, ctypes.POINTER(ctypes.c_char_p)
|
||||
)
|
||||
options.category_denylist_count = len(bytes_denylist)
|
||||
else:
|
||||
options.category_denylist = None
|
||||
options.category_denylist_count = 0
|
||||
|
||||
return options
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Cosine similarity utilities."""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from mediapipe.tasks.python.components.containers import embedding_result
|
||||
|
||||
_Embedding = embedding_result.Embedding
|
||||
|
||||
|
||||
def _compute_cosine_similarity(u: np.ndarray, v: np.ndarray):
|
||||
"""Computes cosine similarity between two embeddings."""
|
||||
|
||||
if len(u) <= 0:
|
||||
raise ValueError("Cannot compute cosing similarity on empty embeddings.")
|
||||
|
||||
norm_u = np.linalg.norm(u)
|
||||
norm_v = np.linalg.norm(v)
|
||||
|
||||
if norm_u <= 0 or norm_v <= 0:
|
||||
raise ValueError(
|
||||
"Cannot compute cosine similarity on embedding with 0 norm.")
|
||||
|
||||
return u.dot(v) / (norm_u * norm_v)
|
||||
|
||||
|
||||
def cosine_similarity(u: _Embedding, v: _Embedding) -> float:
|
||||
"""Utility function to compute cosine similarity between two embedding.
|
||||
|
||||
May return an InvalidArgumentError if e.g. the feature vectors are of
|
||||
different types (quantized vs. float), have different sizes, or have an
|
||||
L2-norm of 0.
|
||||
|
||||
Args:
|
||||
u: An embedding.
|
||||
v: An embedding.
|
||||
|
||||
Returns:
|
||||
Cosine similarity value.
|
||||
"""
|
||||
if len(u.embedding) != len(v.embedding):
|
||||
raise ValueError(f"Cannot compute cosine similarity between embeddings "
|
||||
f"of different sizes "
|
||||
f"({len(u.embedding)} vs. {len(v.embedding)}).")
|
||||
|
||||
if u.embedding.dtype == float and v.embedding.dtype == float:
|
||||
return _compute_cosine_similarity(u.embedding, v.embedding)
|
||||
|
||||
if u.embedding.dtype == np.uint8 and v.embedding.dtype == np.uint8:
|
||||
return _compute_cosine_similarity(
|
||||
u.embedding.view("int8").astype("float"),
|
||||
v.embedding.view("int8").astype("float"),
|
||||
)
|
||||
|
||||
raise ValueError("Cannot compute cosine similarity between quantized and "
|
||||
"float embeddings.")
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
# Copyright 2025 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""A dispatcher to handle asynchronous results from a C library."""
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
import logging
|
||||
import os
|
||||
import queue
|
||||
import threading
|
||||
from typing import Any, Callable, Optional, TypeVar, Union
|
||||
|
||||
from mediapipe.tasks.python.core import mediapipe_c_utils
|
||||
|
||||
CCallbackType = TypeVar("CCallbackType", bound=Callable[..., Any])
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class LiveStreamPacket:
|
||||
"""A packet holding the contents of an async stream packet.
|
||||
|
||||
Attributes:
|
||||
contents: The data that will be passed to the user-provided callback.
|
||||
"""
|
||||
|
||||
contents: tuple[object, ...]
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class _ExceptionPacket:
|
||||
"""A packet holding an exception during an async operation.
|
||||
|
||||
Attributes:
|
||||
exception: The exception that will be logged in the dispatcher thread.
|
||||
"""
|
||||
|
||||
exception: Exception
|
||||
|
||||
|
||||
_AsyncResultPacket = Union[LiveStreamPacket, _ExceptionPacket]
|
||||
|
||||
|
||||
class _DispatcherState(enum.Enum):
|
||||
"""The state of the AsyncResultDispatcher."""
|
||||
|
||||
NOT_STARTED = 0
|
||||
RUNNING = 1
|
||||
SHUTTING_DOWN = 2
|
||||
|
||||
|
||||
_PIPE_NOT_INITIALIZED = -1
|
||||
|
||||
|
||||
class AsyncResultDispatcher:
|
||||
"""A dispatcher to safely handle asynchronous results from a C library.
|
||||
|
||||
This class ensures that the user-provided Python callback is executed on a
|
||||
dedicated, GIL-holding Python thread, rather than directly on the C library's
|
||||
background thread. To use this class, you must supply a converter function
|
||||
that converts the C data types and then pass the Python callback you want to
|
||||
invoke to `wrap_callback()`, which returns a C callback that can be passed
|
||||
to the C library.
|
||||
|
||||
The implementation uses OS level pipes to reduce the amount of Python code
|
||||
that is run without the GIL lock. The dispatcher trhead is only started when
|
||||
the C callback is first called.
|
||||
|
||||
Here's an example of how to use `AsyncResultDispatcher`:
|
||||
|
||||
```
|
||||
def python_callback(result_data: MyResultType, timestamp: int):
|
||||
print(f"Received result at {timestamp}: {result_data}")
|
||||
|
||||
def converter(c_result_ptr: ctypes.POINTER(CResult), c_timestamp: int):
|
||||
py_result = MyResultType.from_ctypes(c_result_ptr[0])
|
||||
py_timestamp = c_timestamp
|
||||
return py_result, py_timestamp
|
||||
|
||||
dispatcher = AsyncResultDispatcher(converter=converter)
|
||||
c_callback_type = ctypes.CFUNCTYPE(
|
||||
None, # Return type (void)
|
||||
ctypes.c_int32, # Status code
|
||||
ctypes.POINTER(CResult), # First argument
|
||||
ctypes.c_int64 # Second argument
|
||||
)
|
||||
c_callback = dispatcher.wrap_callback(python_callback, c_callback_type)
|
||||
```
|
||||
"""
|
||||
|
||||
_callback: Optional[Callable[..., None]] = None
|
||||
_data_queue: queue.Queue[_AsyncResultPacket]
|
||||
_pipe_read_fd: int = _PIPE_NOT_INITIALIZED
|
||||
_pipe_write_fd: int = _PIPE_NOT_INITIALIZED
|
||||
_state: _DispatcherState = _DispatcherState.NOT_STARTED
|
||||
_dispatcher_thread: Optional[threading.Thread] = None
|
||||
|
||||
def __init__(self, converter: Callable[..., Any]):
|
||||
"""Initializes the AsyncResultDispatcher.
|
||||
|
||||
Args:
|
||||
converter: A function that takes the raw C-style arguments from the C
|
||||
library callback and converts them into Python-friendly types.
|
||||
"""
|
||||
self._converter = converter
|
||||
self._data_queue = queue.Queue()
|
||||
|
||||
def wrap_callback(
|
||||
self,
|
||||
python_callback: Optional[Callable[..., None]],
|
||||
c_callback_type: type[CCallbackType],
|
||||
) -> CCallbackType:
|
||||
"""Returns a ctypes callback function that can be passed to a C library.
|
||||
|
||||
This function calls the converter function and schedules the user callback
|
||||
for execution with the converted data. This function can only be called
|
||||
once per instance.
|
||||
|
||||
Args:
|
||||
python_callback: The user-defined result callback for processing live
|
||||
stream data.
|
||||
c_callback_type: The ctypes callback function wrapperof the function you
|
||||
wish to create.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If a callback has already been assigned.
|
||||
"""
|
||||
if python_callback is None:
|
||||
return c_callback_type()
|
||||
|
||||
if self._callback is not None:
|
||||
raise RuntimeError("Callback already set.")
|
||||
self._callback = python_callback
|
||||
|
||||
def c_callback(status_code: int, *args: Any) -> None:
|
||||
"""The callback function that is invoked by the C library.
|
||||
|
||||
Args:
|
||||
status_code: The status code that is passed to the C library callback.
|
||||
*args: Any additional arguments passed to the C library callback.
|
||||
"""
|
||||
if status_code != 0:
|
||||
exception = mediapipe_c_utils.convert_to_exception(status_code)
|
||||
self._put_packet(_ExceptionPacket(exception=exception))
|
||||
return
|
||||
|
||||
try:
|
||||
py_args = self._converter(*args)
|
||||
self._put_packet(LiveStreamPacket(contents=py_args))
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
self._put_packet(_ExceptionPacket(exception=e))
|
||||
|
||||
return c_callback_type(c_callback)
|
||||
|
||||
def _process_packet(self, packet: _AsyncResultPacket) -> None:
|
||||
"""Processes a single packet from the data queue."""
|
||||
if isinstance(packet, _ExceptionPacket):
|
||||
logging.error("Error in async operation: %r", packet.exception)
|
||||
elif isinstance(packet, LiveStreamPacket) and self._callback:
|
||||
try:
|
||||
self._callback(*packet.contents)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
logging.exception("Error in callback: %r", e)
|
||||
|
||||
def _dispatcher_loop(self) -> None:
|
||||
"""The main loop for the dispatcher thread."""
|
||||
while True:
|
||||
# Drain elements that are currently in the queue.
|
||||
while not self._data_queue.empty():
|
||||
packet = self._data_queue.get_nowait()
|
||||
self._process_packet(packet)
|
||||
|
||||
if self._state == _DispatcherState.SHUTTING_DOWN:
|
||||
break
|
||||
|
||||
try:
|
||||
# Block until the C callback writes to the pipe again
|
||||
os.read(self._pipe_read_fd, 1)
|
||||
except OSError:
|
||||
# Pipe might error during shutdown. Ignore.
|
||||
continue
|
||||
|
||||
def _put_packet(self, packet: _AsyncResultPacket) -> None:
|
||||
"""Puts a data packet into the queue and signals the dispatcher thread."""
|
||||
if self._state == _DispatcherState.NOT_STARTED:
|
||||
self._start()
|
||||
elif self._state == _DispatcherState.SHUTTING_DOWN:
|
||||
return
|
||||
|
||||
self._data_queue.put(packet)
|
||||
try:
|
||||
os.write(self._pipe_write_fd, b"\0")
|
||||
except OSError:
|
||||
# Pipe might error during shutdown. Ignore.
|
||||
pass
|
||||
|
||||
def close(self) -> None:
|
||||
"""Shuts down the dispatcher thread and cleans up resources."""
|
||||
if self._state == _DispatcherState.RUNNING:
|
||||
self._state = _DispatcherState.SHUTTING_DOWN
|
||||
try:
|
||||
# Write a final byte to unblock the os.read() call.
|
||||
os.write(self._pipe_write_fd, b"\0")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if self._dispatcher_thread:
|
||||
self._dispatcher_thread.join()
|
||||
os.close(self._pipe_read_fd)
|
||||
os.close(self._pipe_write_fd)
|
||||
|
||||
def _start(self) -> None:
|
||||
"""Starts the dispatcher thread."""
|
||||
if self._callback is None:
|
||||
raise ValueError("Dispatcher should not be started without a callback.")
|
||||
self._state = _DispatcherState.RUNNING
|
||||
self._pipe_read_fd, self._pipe_write_fd = os.pipe()
|
||||
self._dispatcher_thread = threading.Thread(
|
||||
target=self._dispatcher_loop, daemon=True
|
||||
)
|
||||
self._dispatcher_thread.start()
|
||||
@@ -0,0 +1,125 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Base options for MediaPipe Task APIs."""
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
import platform
|
||||
import sys
|
||||
import types
|
||||
from typing import Any, Optional
|
||||
|
||||
import certifi
|
||||
|
||||
from mediapipe.tasks.python.core import base_options_c as base_options_c_lib
|
||||
from mediapipe.tasks.python.core.optional_dependencies import doc_controls
|
||||
|
||||
|
||||
|
||||
# C enum value for host environment.
|
||||
HOST_ENVIRONMENT_PYTHON: int = 3
|
||||
|
||||
|
||||
class _HostSystem(enum.IntEnum):
|
||||
"""An mapping for the C enum values for the host operating system.
|
||||
|
||||
Attributes:
|
||||
HOST_SYSTEM_UNKNOWN: Unknown host system.
|
||||
HOST_SYSTEM_LINUX: Linux host system.
|
||||
HOST_SYSTEM_MAC: MacOS host system.
|
||||
HOST_SYSTEM_WINDOWS: Windows host system.
|
||||
"""
|
||||
|
||||
HOST_SYSTEM_UNKNOWN = 0
|
||||
HOST_SYSTEM_LINUX = 1
|
||||
HOST_SYSTEM_MAC = 2
|
||||
HOST_SYSTEM_WINDOWS = 3
|
||||
|
||||
|
||||
|
||||
|
||||
_HOST_SYSTEM_BY_PLATFORM = types.MappingProxyType({
|
||||
'Darwin': _HostSystem.HOST_SYSTEM_MAC,
|
||||
'Windows': _HostSystem.HOST_SYSTEM_WINDOWS,
|
||||
'Linux': _HostSystem.HOST_SYSTEM_LINUX,
|
||||
})
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class BaseOptions:
|
||||
"""Base options for MediaPipe Tasks' Python APIs.
|
||||
|
||||
Represents external model asset used by the Task APIs. The files can be
|
||||
specified by one of the following two ways:
|
||||
|
||||
(1) model asset file path in `model_asset_path`.
|
||||
(2) model asset contents loaded in `model_asset_buffer`.
|
||||
|
||||
If more than one field of these fields is provided, they are used in this
|
||||
precedence order.
|
||||
|
||||
Attributes:
|
||||
model_asset_path: Path to the model asset file.
|
||||
model_asset_buffer: The model asset file contents as bytes.
|
||||
delegate: Acceleration to use. Supported values are GPU and CPU. GPU support
|
||||
is currently limited to Ubuntu platforms.
|
||||
"""
|
||||
|
||||
class Delegate(enum.IntEnum):
|
||||
CPU = 0
|
||||
GPU = 1
|
||||
|
||||
model_asset_path: Optional[str] = None
|
||||
model_asset_buffer: Optional[bytes] = None
|
||||
delegate: Optional[Delegate] = None
|
||||
|
||||
@doc_controls.do_not_generate_docs
|
||||
def to_ctypes(self) -> base_options_c_lib.BaseOptionsC:
|
||||
"""Creates a BaseOptionsC struct from the BaseOptions object."""
|
||||
ca_bundle_path = certifi.where()
|
||||
host_system = _HOST_SYSTEM_BY_PLATFORM.get(
|
||||
platform.system(), _HostSystem.HOST_SYSTEM_UNKNOWN
|
||||
)
|
||||
options = base_options_c_lib.BaseOptionsC()
|
||||
options.model_asset_buffer = self.model_asset_buffer
|
||||
options.model_asset_buffer_count = (
|
||||
len(self.model_asset_buffer) if self.model_asset_buffer else 0
|
||||
)
|
||||
options.model_asset_path = (
|
||||
self.model_asset_path.encode('utf-8') if self.model_asset_path else None
|
||||
)
|
||||
options.delegate = self.delegate.value if self.delegate else 0
|
||||
options.host_environment = HOST_ENVIRONMENT_PYTHON
|
||||
host_version, *_ = sys.version.split(' ')
|
||||
options.host_system = host_system
|
||||
options.host_version = host_version.encode('utf-8')
|
||||
options.ca_bundle_path = ca_bundle_path.encode('utf-8')
|
||||
return options
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Checks if this object is equal to the given object.
|
||||
|
||||
Args:
|
||||
other: The object to be compared with.
|
||||
|
||||
Returns:
|
||||
True if the objects are equal.
|
||||
"""
|
||||
if not isinstance(other, BaseOptions):
|
||||
return False
|
||||
return (
|
||||
self.model_asset_path == other.model_asset_path
|
||||
and self.model_asset_buffer == other.model_asset_buffer
|
||||
and self.delegate == other.delegate
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
# Copyright 2025 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""C types for BaseOptions."""
|
||||
|
||||
import ctypes
|
||||
|
||||
|
||||
class BaseOptionsC(ctypes.Structure):
|
||||
"""C types for BaseOptions.
|
||||
|
||||
Attributes:
|
||||
model_asset_buffer: `bytes`, the model asset buffer.
|
||||
model_asset_buffer_count: `int`, the size of the model asset buffer.
|
||||
model_asset_path: `bytes`, the path to the model asset.
|
||||
delegate: `int`, the delegate to use.
|
||||
host_environment: `int`, the environment in which the task is running.
|
||||
host_system: `int`, the system on which the task is running.
|
||||
host_version: `bytes`, the Python version as a UTF-8 string.
|
||||
ca_bundle_path: `bytes`, the path to the CA bundle file as a UTF-8 string.
|
||||
"""
|
||||
_fields_ = [
|
||||
("model_asset_buffer", ctypes.c_char_p),
|
||||
("model_asset_buffer_count", ctypes.c_uint),
|
||||
("model_asset_path", ctypes.c_char_p),
|
||||
("delegate", ctypes.c_int),
|
||||
("host_environment", ctypes.c_int),
|
||||
("host_system", ctypes.c_int),
|
||||
("host_version", ctypes.c_char_p),
|
||||
("ca_bundle_path", ctypes.c_char_p),
|
||||
]
|
||||
@@ -0,0 +1,105 @@
|
||||
# Copyright 2025 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""MediaPipe Text tasks shared library."""
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import platform
|
||||
from typing import Any, List, Optional, Sequence
|
||||
|
||||
from importlib import resources
|
||||
from mediapipe.tasks.python.core import mediapipe_c_utils
|
||||
from mediapipe.tasks.python.core import serial_dispatcher
|
||||
|
||||
_BASE_LIB_PATH = 'mediapipe/tasks/c/'
|
||||
_shared_lib = None
|
||||
_CFunction = mediapipe_c_utils.CFunction
|
||||
|
||||
|
||||
def load_raw_library(signatures: Sequence[_CFunction] = ()) -> ctypes.CDLL:
|
||||
"""Loads the raw ctypes.CDLL shared library and registers signatures.
|
||||
|
||||
This function loads the raw ctypes.CDLL shared library if it hasn't been
|
||||
loaded yet, otherwise it re-uses the existing instance. It attaches the
|
||||
provided signatures and makes them available across all callsites.
|
||||
|
||||
Args:
|
||||
signatures: The ctypes function signatures to register in the library.
|
||||
|
||||
Returns:
|
||||
The ctypes shared library.
|
||||
"""
|
||||
global _shared_lib
|
||||
if _shared_lib is None:
|
||||
if os.name == 'posix':
|
||||
if platform.system() == 'Darwin': # macOS
|
||||
lib_filename = 'libmediapipe.dylib'
|
||||
else: # Linux
|
||||
lib_filename = 'libmediapipe.so'
|
||||
else: # Windows
|
||||
lib_filename = 'libmediapipe.dll'
|
||||
lib_path_context = resources.files('mediapipe.tasks.c')
|
||||
absolute_lib_path = str(lib_path_context / lib_filename)
|
||||
_shared_lib = ctypes.CDLL(absolute_lib_path)
|
||||
|
||||
for signature in signatures:
|
||||
c_func = getattr(_shared_lib, signature.func_name)
|
||||
c_func.argtypes = signature.argtypes
|
||||
c_func.restype = signature.restype
|
||||
|
||||
# Register "MpErrorFree()"
|
||||
_shared_lib.MpErrorFree.argtypes = [ctypes.c_void_p]
|
||||
_shared_lib.MpErrorFree.restype = None
|
||||
|
||||
return _shared_lib
|
||||
|
||||
|
||||
def load_shared_library(
|
||||
signatures: Sequence[_CFunction] = (),
|
||||
) -> serial_dispatcher.SerialDispatcher:
|
||||
"""Loads the shared library in a SerialDispatcher and registers signatures.
|
||||
|
||||
This function creates a thread-safe wrapper for the shared library that
|
||||
dispatches all calls through a single dedicated thread. Every call to this
|
||||
function uses a different dispatch library with its own thread.
|
||||
|
||||
Args:
|
||||
signatures: The ctypes function signatures to register in the library.
|
||||
|
||||
Returns:
|
||||
A thread-safe wrapper for the ctypes shared library.
|
||||
"""
|
||||
raw_lib = load_raw_library()
|
||||
return serial_dispatcher.SerialDispatcher(raw_lib, signatures)
|
||||
|
||||
|
||||
def convert_strings_to_ctypes_array(
|
||||
str_list: Optional[List[str]],
|
||||
) -> Optional[Any]:
|
||||
"""Converts a list of Python strings to a ctypes array of c_char_p.
|
||||
|
||||
Args:
|
||||
str_list: A list of strings, or None.
|
||||
|
||||
Returns:
|
||||
A ctypes array of c_char_p, or None if the input is None or empty.
|
||||
"""
|
||||
if not str_list: # Handles None and empty list
|
||||
return None
|
||||
num_elements = len(str_list)
|
||||
c_array = (ctypes.c_char_p * num_elements)()
|
||||
encoded_strings = [s.encode('utf-8') for s in str_list]
|
||||
c_array[:] = encoded_strings
|
||||
|
||||
return c_array
|
||||
@@ -0,0 +1,232 @@
|
||||
# Copyright 2025 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Common MediaPipe CTypes definitions and utilities."""
|
||||
|
||||
import atexit
|
||||
import concurrent.futures
|
||||
import ctypes
|
||||
import dataclasses
|
||||
import enum
|
||||
import functools
|
||||
import threading
|
||||
from typing import Any, Callable, Optional, Sequence, Union
|
||||
|
||||
|
||||
# Global shutdown state to ignore calls during Python shutdown. This is used to
|
||||
# prevent forwarding calls to the underlying executor, which prints warning logs
|
||||
# to the console after shutdown.
|
||||
_shutdown = False
|
||||
_global_shutdown_lock = threading.Lock()
|
||||
|
||||
|
||||
def _python_exit():
|
||||
global _shutdown
|
||||
with _global_shutdown_lock:
|
||||
_shutdown = True
|
||||
|
||||
|
||||
def is_shutdown():
|
||||
with _global_shutdown_lock:
|
||||
return _shutdown
|
||||
|
||||
|
||||
atexit.register(_python_exit)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class CFunction:
|
||||
"""Stores the ctypes signature information for a C function.
|
||||
|
||||
Attributes:
|
||||
func_name: The name of the C function.
|
||||
argtypes: The ctypes types for the function arguments.
|
||||
restype: The ctypes type for the function's return value.
|
||||
"""
|
||||
|
||||
func_name: str
|
||||
argtypes: list[Any]
|
||||
restype: Any
|
||||
|
||||
def create_python_wrapper(
|
||||
self, lib: ctypes.CDLL, executor: concurrent.futures.Executor
|
||||
) -> Callable[..., Any]:
|
||||
"""Creates a callable wrapper that executes the C function on the executor.
|
||||
|
||||
Args:
|
||||
lib: The ctypes shared library.
|
||||
executor: The executor to use for the C function.
|
||||
|
||||
Returns:
|
||||
A callable that executes the C function on the executor.
|
||||
"""
|
||||
c_func = getattr(lib, self.func_name)
|
||||
c_func.argtypes = self.argtypes
|
||||
c_func.restype = self.restype
|
||||
|
||||
@functools.wraps(c_func)
|
||||
def dispatcher_wrapper(*args, **kwargs) -> Any:
|
||||
future = executor.submit(c_func, *args, **kwargs)
|
||||
return future.result()
|
||||
|
||||
return dispatcher_wrapper
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class CStatusFunction(CFunction):
|
||||
"""Stores the ctypes signature for a status-returning C function.
|
||||
|
||||
The functions is required to take an error message argument as the last
|
||||
argument and return an `MpStatus' integer code.
|
||||
"""
|
||||
|
||||
def __init__(self, func_name: str, core_argtypes: Sequence[Any]):
|
||||
"""Initializes the CStatusFunction.
|
||||
|
||||
Args:
|
||||
func_name: The name of the C function.
|
||||
core_argtypes: The ctypes types for the function arguments excluding the
|
||||
error message argument.
|
||||
"""
|
||||
full_argtypes = list(core_argtypes) + [ctypes.POINTER(ctypes.c_char_p)]
|
||||
super().__init__(func_name, full_argtypes, restype=ctypes.c_int)
|
||||
|
||||
def create_python_wrapper(
|
||||
self, lib: ctypes.CDLL, executor: concurrent.futures.Executor
|
||||
) -> Callable[..., None]:
|
||||
"""Creates a callable wrapper that executes the C function on the executor.
|
||||
|
||||
This function raises a Python exception based on the MpStatus code returned
|
||||
by the C API function and the error message argument.
|
||||
|
||||
Args:
|
||||
lib: The ctypes shared library.
|
||||
executor: The executor to use for the C function.
|
||||
|
||||
Returns:
|
||||
A callable that executes the C function on the executor.
|
||||
"""
|
||||
c_func = getattr(lib, self.func_name)
|
||||
c_func.argtypes = self.argtypes
|
||||
c_func.restype = self.restype
|
||||
error_msg_index = len(self.argtypes)
|
||||
|
||||
@functools.wraps(c_func)
|
||||
def dispatcher_wrapper(*args, **kwargs) -> None:
|
||||
if is_shutdown():
|
||||
return
|
||||
|
||||
def dispatch_and_free():
|
||||
# Add the error message argument to the list of arguments.
|
||||
error_msg = ctypes.c_char_p()
|
||||
call_args = list(args)
|
||||
call_args.insert(error_msg_index, ctypes.byref(error_msg))
|
||||
|
||||
status = c_func(*call_args, **kwargs)
|
||||
try:
|
||||
handle_status(status, error_msg)
|
||||
finally:
|
||||
if error_msg.value is not None:
|
||||
lib.MpErrorFree(error_msg)
|
||||
|
||||
executor.submit(dispatch_and_free).result()
|
||||
|
||||
return dispatcher_wrapper
|
||||
|
||||
|
||||
class MpStatus(enum.IntEnum):
|
||||
"""Status codes for MediaPipe C API functions."""
|
||||
|
||||
MP_OK = 0
|
||||
MP_CANCELLED = 1
|
||||
MP_UNKNOWN = 2
|
||||
MP_INVALID_ARGUMENT = 3
|
||||
MP_DEADLINE_EXCEEDED = 4
|
||||
MP_NOT_FOUND = 5
|
||||
MP_ALREADY_EXISTS = 6
|
||||
MP_PERMISSION_DENIED = 7
|
||||
MP_RESOURCE_EXHAUSTED = 8
|
||||
MP_FAILED_PRECONDITION = 9
|
||||
MP_ABORTED = 10
|
||||
MP_OUT_OF_RANGE = 11
|
||||
MP_UNIMPLEMENTED = 12
|
||||
MP_INTERNAL = 13
|
||||
MP_UNAVAILABLE = 14
|
||||
MP_DATA_LOSS = 15
|
||||
MP_UNAUTHENTICATED = 16
|
||||
|
||||
|
||||
def convert_to_exception(
|
||||
status: int, error_message: Optional[ctypes.c_char_p] = None
|
||||
) -> Union[Exception, None]:
|
||||
"""Returns an exception based on the MpStatus code, or None if MP_OK."""
|
||||
error_str = (
|
||||
error_message.value.decode('utf-8')
|
||||
if error_message and error_message.value is not None
|
||||
else None
|
||||
)
|
||||
|
||||
if status == MpStatus.MP_OK:
|
||||
return None
|
||||
elif status == MpStatus.MP_CANCELLED:
|
||||
return TimeoutError(error_str or 'Cancelled')
|
||||
elif status == MpStatus.MP_UNKNOWN:
|
||||
return RuntimeError(error_str or 'Unknown error')
|
||||
elif status == MpStatus.MP_INVALID_ARGUMENT:
|
||||
return ValueError(error_str or 'Invalid argument')
|
||||
elif status == MpStatus.MP_DEADLINE_EXCEEDED:
|
||||
return TimeoutError(error_str or 'Deadline exceeded')
|
||||
elif status == MpStatus.MP_NOT_FOUND:
|
||||
return FileNotFoundError(error_str or 'Not found')
|
||||
elif status == MpStatus.MP_ALREADY_EXISTS:
|
||||
return FileExistsError(error_str or 'Already exists')
|
||||
elif status == MpStatus.MP_PERMISSION_DENIED:
|
||||
return PermissionError(error_str or 'Permission denied')
|
||||
elif status == MpStatus.MP_RESOURCE_EXHAUSTED:
|
||||
return RuntimeError(error_str or 'Resource exhausted')
|
||||
elif status == MpStatus.MP_FAILED_PRECONDITION:
|
||||
return RuntimeError(error_str or 'Failed precondition')
|
||||
elif status == MpStatus.MP_ABORTED:
|
||||
return RuntimeError(error_str or 'Aborted')
|
||||
elif status == MpStatus.MP_OUT_OF_RANGE:
|
||||
return IndexError(error_str or 'Out of range')
|
||||
elif status == MpStatus.MP_UNIMPLEMENTED:
|
||||
return NotImplementedError(error_str or 'Unimplemented')
|
||||
elif status == MpStatus.MP_INTERNAL:
|
||||
return RuntimeError(error_str or 'Internal error')
|
||||
elif status == MpStatus.MP_UNAVAILABLE:
|
||||
return RuntimeError(error_str or 'Unavailable')
|
||||
elif status == MpStatus.MP_DATA_LOSS:
|
||||
return RuntimeError(error_str or 'Data loss')
|
||||
elif status == MpStatus.MP_UNAUTHENTICATED:
|
||||
return PermissionError(error_str or 'Unauthenticated')
|
||||
else:
|
||||
return RuntimeError(error_str or f'Unexpected status: {status}')
|
||||
|
||||
|
||||
def handle_status(
|
||||
status: int, error_message: Optional[ctypes.c_char_p] = None
|
||||
) -> None:
|
||||
"""Checks the MpStatus and raises an error if not kMpOk.
|
||||
|
||||
Args:
|
||||
status: The MpStatus code to check.
|
||||
error_message: The error message to use if an exception is raised. Uses a
|
||||
default error message if None.
|
||||
|
||||
Raises:
|
||||
An error if the MpStatus is not kMpOk.
|
||||
"""
|
||||
exception = convert_to_exception(status, error_message)
|
||||
if exception:
|
||||
raise exception
|
||||
@@ -0,0 +1,25 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""MediaPipe Tasks' common but optional dependencies."""
|
||||
|
||||
# TensorFlow isn't a dependency of mediapipe pip package. It's only
|
||||
# required in the API docgen pipeline so we'll ignore it if tensorflow is not
|
||||
# installed.
|
||||
try:
|
||||
from tensorflow.tools.docs import doc_controls
|
||||
except ModuleNotFoundError:
|
||||
# Replace the real doc_controls.do_not_generate_docs with an no-op
|
||||
doc_controls = lambda: None
|
||||
no_op = lambda x: x
|
||||
setattr(doc_controls, 'do_not_generate_docs', no_op)
|
||||
@@ -0,0 +1,91 @@
|
||||
# Copyright 2025 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""A thread-safe dispatcher for a ctypes."""
|
||||
|
||||
import concurrent.futures
|
||||
import ctypes
|
||||
import threading
|
||||
from typing import Any, Sequence
|
||||
|
||||
from mediapipe.tasks.python.core import mediapipe_c_utils
|
||||
|
||||
|
||||
class SerialDispatcher:
|
||||
"""A wrapper class for a ctypes.CDLL object that serializes all calls.
|
||||
|
||||
This ensures that functions from a non-thread-safe C library are called
|
||||
sequentially from a single dedicated thread, preventing race conditions
|
||||
and segmentation faults.
|
||||
|
||||
If a function is a CStatusFunction, the dispatcher will raise a Python
|
||||
exception if the returned MpStatus code is not kMpOk.
|
||||
"""
|
||||
|
||||
# Enable dynamic attributes as we register methods on this class via
|
||||
# reflection. This allows these methods to be used with type checking.
|
||||
_HAS_DYNAMIC_ATTRIBUTES = True
|
||||
|
||||
def __init__(
|
||||
self, lib: ctypes.CDLL, signatures: Sequence[mediapipe_c_utils.CFunction]
|
||||
):
|
||||
"""Initializes the SerialDispatcher.
|
||||
|
||||
Args:
|
||||
lib: The ctypes.CDLL object to wrap.
|
||||
signatures: the CFunction objects specifying the functions to wrap.
|
||||
"""
|
||||
self._lib = lib
|
||||
self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
||||
self._lock = threading.Lock()
|
||||
self._is_closed = False
|
||||
|
||||
for signature in signatures:
|
||||
self._register_signature(signature)
|
||||
|
||||
def _register_signature(self, signature: mediapipe_c_utils.CFunction):
|
||||
"""Registers a wrapped C function as a method on the SerialDispatcher.
|
||||
|
||||
This method attaches a wrapper method to this class that dispatches all
|
||||
calls to the underlying C function through this class' executor. The
|
||||
methods are directly attached to the SerialDispatcher object, allowing them
|
||||
to be used transparently.
|
||||
|
||||
Args:
|
||||
signature: The CFunction object specifying the C function to wrap and
|
||||
register.
|
||||
"""
|
||||
handler = signature.create_python_wrapper(self._lib, self._executor)
|
||||
def shutdown_aware_handler(*args, **kwargs) -> Any:
|
||||
with self._lock:
|
||||
if self._is_closed:
|
||||
return
|
||||
return handler(*args, **kwargs)
|
||||
|
||||
setattr(self, signature.func_name, shutdown_aware_handler)
|
||||
|
||||
def close(self):
|
||||
"""Shuts down the dispatcher and waits for pending tasks to complete."""
|
||||
with self._lock:
|
||||
if self._is_closed:
|
||||
return
|
||||
self._is_closed = True
|
||||
self._executor.shutdown(wait=True)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
del exc_type, exc_val, exc_tb
|
||||
self.close()
|
||||
@@ -0,0 +1,14 @@
|
||||
# Copyright 2024 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""MediaPipe GenAI Python API."""
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright 2024 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""MediaPipe Tasks GenAI Bundler API."""
|
||||
|
||||
import mediapipe.tasks.python.genai.bundler.llm_bundler
|
||||
|
||||
BundleConfig = llm_bundler.BundleConfig
|
||||
create_bundle = llm_bundler.create_bundle
|
||||
|
||||
# Remove unnecessary modules to avoid duplication in API docs.
|
||||
del llm_bundler
|
||||
@@ -0,0 +1,236 @@
|
||||
# Copyright 2024 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Functions to perform llm packing."""
|
||||
|
||||
import ctypes
|
||||
import dataclasses
|
||||
import enum
|
||||
from typing import List, Optional
|
||||
|
||||
from mediapipe.tasks.python.core import mediapipe_c_bindings
|
||||
from mediapipe.tasks.python.core import mediapipe_c_utils
|
||||
from mediapipe.tasks.python.core import serial_dispatcher
|
||||
from mediapipe.tasks.python.genai.bundler import llm_bundler_metadata_options
|
||||
from mediapipe.tasks.python.genai.bundler import llm_bundler_metadata_options_c
|
||||
from mediapipe.tasks.python.genai.converter import external_dependencies
|
||||
from mediapipe.tasks.python.metadata.metadata_writers import model_asset_bundle_utils
|
||||
|
||||
sentencepiece = external_dependencies.sentencepiece
|
||||
|
||||
_CTYPES_SIGNATURES = (
|
||||
mediapipe_c_utils.CFunction(
|
||||
func_name="MpLlmBundlerGenerateMetadata",
|
||||
argtypes=[
|
||||
ctypes.POINTER(
|
||||
llm_bundler_metadata_options_c.LlmBundlerMetadataOptionsC
|
||||
),
|
||||
ctypes.POINTER(ctypes.c_int),
|
||||
],
|
||||
restype=ctypes.POINTER(ctypes.c_char),
|
||||
),
|
||||
mediapipe_c_utils.CFunction(
|
||||
func_name="MpLlmBundlerFreeMetadata",
|
||||
argtypes=[
|
||||
ctypes.POINTER(ctypes.c_char),
|
||||
],
|
||||
restype=None,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class BundleConfig:
|
||||
"""Config for LLM Bundler.
|
||||
|
||||
Attributes:
|
||||
tflite_model: Path to the multi-signature tflite model with "prefill" and
|
||||
"decode" signatures converted using ODML Transformers APIs.
|
||||
tokenizer_model: Path to the tokenizer model. Currently only SentencePience
|
||||
tokenizer is supported. As such, tokenizer.model proto is expected to be
|
||||
passed here.
|
||||
start_token: Token that will be used to signify the beginning of a sequence.
|
||||
stop_tokens: Tokens that will be used to signify the end of a sequence.
|
||||
output_filename: Name of the generated `.task` file containing the Bundle.
|
||||
enable_bytes_to_unicode_mapping: Enables GPT-2 style bytes to unicode
|
||||
mapping. For more details see:
|
||||
https://github.com/openai/gpt-2/blob/master/src/encoder.py#L9
|
||||
system_prompt: The starting text would be feed to the model on the start of
|
||||
each session, commonly called the System prompt. This is useful for
|
||||
instruction tuned models and pre-conditioning the model behavior. This
|
||||
field is mutually exclusive with prompt_prefix_system and
|
||||
prompt_suffix_system.
|
||||
prompt_prefix_user: The prefix that should be prepended to each user portion
|
||||
of prompts passed to the model.
|
||||
prompt_suffix_user: The suffix that should be appended to each user portion
|
||||
of prompts passed to the model.
|
||||
prompt_prefix_model: The prefix that should be prepended to each model
|
||||
portion of prompts passed to the model.
|
||||
prompt_suffix_model: The suffix that should be appended to each model
|
||||
portion of prompts passsed to the model.
|
||||
prompt_prefix_system: The prefix that should be prepended to each system
|
||||
portion of prompts passed to the model.
|
||||
prompt_suffix_system: The suffix that should be appended to each system
|
||||
portion of prompts passsed to the model.
|
||||
user_role_token: The token that indicates the user's turn.
|
||||
system_role_token: The token that indicates the system's turn.
|
||||
model_role_token: The token that indicates the model's turn.
|
||||
end_role_token: The token that indicates the end of the prompt.
|
||||
tflite_embedder: Path to the embedding model if the embedding lookup is done
|
||||
externally.
|
||||
tflite_per_layer_embedder: Path to the per layer embedding model if the per
|
||||
layer embedding lookup is done externally.
|
||||
tflite_vision_encoder: Path to the vision encoder model.
|
||||
tflite_vision_adapter: Path to the vision adapter model.
|
||||
"""
|
||||
|
||||
tflite_model: str
|
||||
tokenizer_model: str
|
||||
start_token: str
|
||||
stop_tokens: List[str]
|
||||
output_filename: str
|
||||
enable_bytes_to_unicode_mapping: bool = False
|
||||
system_prompt: Optional[str] = None
|
||||
prompt_prefix_user: Optional[str] = None
|
||||
prompt_suffix_user: Optional[str] = None
|
||||
prompt_prefix_model: Optional[str] = None
|
||||
prompt_suffix_model: Optional[str] = None
|
||||
prompt_prefix_system: Optional[str] = None
|
||||
prompt_suffix_system: Optional[str] = None
|
||||
user_role_token: Optional[str] = None
|
||||
system_role_token: Optional[str] = None
|
||||
model_role_token: Optional[str] = None
|
||||
end_role_token: Optional[str] = None
|
||||
tflite_embedder: Optional[str] = None
|
||||
tflite_per_layer_embedder: Optional[str] = None
|
||||
tflite_vision_encoder: Optional[str] = None
|
||||
tflite_vision_adapter: Optional[str] = None
|
||||
|
||||
|
||||
class _BundleTags(enum.Enum):
|
||||
"""Bundle tags."""
|
||||
|
||||
TF_LITE_PREFILL_DECODE = 1
|
||||
TOKENIZER_MODEL = 2
|
||||
METADATA = 3
|
||||
TF_LITE_EMBEDDER = 4
|
||||
TF_LITE_PER_LAYER_EMBEDDER = 5
|
||||
TF_LITE_VISION_ENCODER = 6
|
||||
TF_LITE_VISION_ADAPTER = 7
|
||||
|
||||
|
||||
def _validate_config(config: BundleConfig):
|
||||
"""Validates the given config.
|
||||
|
||||
Args:
|
||||
config: The config to validate.
|
||||
|
||||
Raises:
|
||||
ValueError if the config is invalid. Invalid configs can be:
|
||||
- tokenizer_model is not a valid SentencePiece model.
|
||||
- start_token is not a valid token in the tokenizer_model.
|
||||
- stop_tokens is not a list of valid tokens in the tokenizer_model.
|
||||
- system_prompt and prompt_*_system are both set.
|
||||
"""
|
||||
if not isinstance(config.stop_tokens, list):
|
||||
raise ValueError("stop_tokens must be a list of strings.")
|
||||
if not config.stop_tokens:
|
||||
raise ValueError("stop_tokens must be non-empty.")
|
||||
|
||||
try:
|
||||
sp = sentencepiece.SentencePieceProcessor()
|
||||
sp.Load(config.tokenizer_model)
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
f"Failed to load tokenizer model from {config.tokenizer_model}. "
|
||||
"Please ensure you are passing a valid SentencePiece model."
|
||||
) from e
|
||||
|
||||
if config.system_prompt and (
|
||||
config.prompt_prefix_system or config.prompt_suffix_system
|
||||
):
|
||||
raise ValueError(
|
||||
"system_prompt and prompt_*_system are mutually exclusive."
|
||||
)
|
||||
|
||||
|
||||
def create_bundle(config: BundleConfig):
|
||||
"""Creates a bundle from the given config."""
|
||||
_validate_config(config)
|
||||
|
||||
artifacts = {}
|
||||
with open(config.tflite_model, "rb") as f:
|
||||
artifacts[_BundleTags.TF_LITE_PREFILL_DECODE.name] = f.read()
|
||||
|
||||
with open(config.tokenizer_model, "rb") as f:
|
||||
artifacts[_BundleTags.TOKENIZER_MODEL.name] = f.read()
|
||||
|
||||
lib = mediapipe_c_bindings.load_raw_library(_CTYPES_SIGNATURES)
|
||||
with serial_dispatcher.SerialDispatcher(
|
||||
lib, _CTYPES_SIGNATURES
|
||||
) as bundler_lib:
|
||||
metadata_options = llm_bundler_metadata_options.LlmBundlerMetadataOptions(
|
||||
start_token=config.start_token,
|
||||
stop_tokens=config.stop_tokens,
|
||||
enable_bytes_to_unicode_mapping=config.enable_bytes_to_unicode_mapping,
|
||||
system_prompt=config.system_prompt,
|
||||
prompt_prefix_user=config.prompt_prefix_user,
|
||||
prompt_suffix_user=config.prompt_suffix_user,
|
||||
prompt_prefix_model=config.prompt_prefix_model,
|
||||
prompt_suffix_model=config.prompt_suffix_model,
|
||||
prompt_prefix_system=config.prompt_prefix_system,
|
||||
prompt_suffix_system=config.prompt_suffix_system,
|
||||
user_role_token=config.user_role_token,
|
||||
system_role_token=config.system_role_token,
|
||||
model_role_token=config.model_role_token,
|
||||
end_role_token=config.end_role_token,
|
||||
)
|
||||
metadata_options_c = metadata_options.to_ctypes()
|
||||
buffer_size = ctypes.c_int()
|
||||
metadata_buffer = None
|
||||
try:
|
||||
metadata_buffer = bundler_lib.MpLlmBundlerGenerateMetadata(
|
||||
ctypes.byref(metadata_options_c), ctypes.byref(buffer_size)
|
||||
)
|
||||
artifacts[_BundleTags.METADATA.name] = ctypes.string_at(
|
||||
metadata_buffer, buffer_size.value
|
||||
)
|
||||
finally:
|
||||
if metadata_buffer:
|
||||
bundler_lib.MpLlmBundlerFreeMetadata(metadata_buffer)
|
||||
|
||||
if config.tflite_embedder:
|
||||
with open(config.tflite_embedder, "rb") as f:
|
||||
artifacts[_BundleTags.TF_LITE_EMBEDDER.name] = f.read()
|
||||
|
||||
if config.tflite_per_layer_embedder:
|
||||
with open(config.tflite_per_layer_embedder, "rb") as f:
|
||||
artifacts[_BundleTags.TF_LITE_PER_LAYER_EMBEDDER.name] = f.read()
|
||||
|
||||
if config.tflite_vision_encoder:
|
||||
with open(config.tflite_vision_encoder, "rb") as f:
|
||||
artifacts[_BundleTags.TF_LITE_VISION_ENCODER.name] = f.read()
|
||||
|
||||
if config.tflite_vision_adapter:
|
||||
with open(config.tflite_vision_adapter, "rb") as f:
|
||||
artifacts[_BundleTags.TF_LITE_VISION_ADAPTER.name] = f.read()
|
||||
|
||||
output_filename = config.output_filename
|
||||
if not output_filename.endswith(".task"):
|
||||
output_filename = config.output_filename + ".task"
|
||||
|
||||
model_asset_bundle_utils.create_model_asset_bundle(
|
||||
artifacts,
|
||||
output_filename,
|
||||
)
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
# Copyright 2026 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""LlmBundlerMetadataOptions Python dataclass."""
|
||||
|
||||
import ctypes
|
||||
import dataclasses
|
||||
from typing import Optional
|
||||
|
||||
from mediapipe.tasks.python.genai.bundler import llm_bundler_metadata_options_c
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class LlmBundlerMetadataOptions:
|
||||
"""Options for bundling LLM metadata.
|
||||
|
||||
Attributes:
|
||||
start_token: Token that will be used to signify the beginning of a sequence.
|
||||
stop_tokens: Tokens that will be used to signify the end of a sequence.
|
||||
enable_bytes_to_unicode_mapping: Enables GPT-2 style bytes to unicode
|
||||
mapping. For more details see:
|
||||
https://github.com/openai/gpt-2/blob/master/src/encoder.py#L9
|
||||
system_prompt: The starting text would be feed to the model on the start of
|
||||
each session, commonly called the System prompt. This is useful for
|
||||
instruction tuned models and pre-conditioning the model behavior. This
|
||||
field is mutually exclusive with prompt_prefix_system and
|
||||
prompt_suffix_system.
|
||||
prompt_prefix_user: The prefix that should be prepended to each user portion
|
||||
of prompts passed to the model.
|
||||
prompt_suffix_user: The suffix that should be appended to each user portion
|
||||
of prompts passed to the model.
|
||||
prompt_prefix_model: The prefix that should be prepended to each model
|
||||
portion of prompts passed to the model.
|
||||
prompt_suffix_model: The suffix that should be appended to each model
|
||||
portion of prompts passsed to the model.
|
||||
prompt_prefix_system: The prefix that should be prepended to each system
|
||||
portion of prompts passed to the model.
|
||||
prompt_suffix_system: The suffix that should be appended to each system
|
||||
portion of prompts passsed to the model.
|
||||
user_role_token: The token that indicates the user's turn.
|
||||
system_role_token: The token that indicates the system's turn.
|
||||
model_role_token: The token that indicates the model's turn.
|
||||
end_role_token: The token that indicates the end of the prompt.
|
||||
"""
|
||||
|
||||
start_token: str
|
||||
stop_tokens: list[str]
|
||||
enable_bytes_to_unicode_mapping: bool = False
|
||||
system_prompt: Optional[str] = None
|
||||
prompt_prefix_user: Optional[str] = None
|
||||
prompt_suffix_user: Optional[str] = None
|
||||
prompt_prefix_model: Optional[str] = None
|
||||
prompt_suffix_model: Optional[str] = None
|
||||
prompt_prefix_system: Optional[str] = None
|
||||
prompt_suffix_system: Optional[str] = None
|
||||
user_role_token: Optional[str] = None
|
||||
system_role_token: Optional[str] = None
|
||||
model_role_token: Optional[str] = None
|
||||
end_role_token: Optional[str] = None
|
||||
|
||||
def to_ctypes(
|
||||
self,
|
||||
) -> llm_bundler_metadata_options_c.LlmBundlerMetadataOptionsC:
|
||||
"""Converts the object to a ctypes structure."""
|
||||
stop_tokens_c = (ctypes.c_char_p * len(self.stop_tokens))()
|
||||
for i, token in enumerate(self.stop_tokens):
|
||||
stop_tokens_c[i] = token.encode("utf-8")
|
||||
start_token_c = self.start_token.encode("utf-8")
|
||||
system_prompt_c = (
|
||||
self.system_prompt.encode("utf-8") if self.system_prompt else None
|
||||
)
|
||||
prompt_prefix_user_c = (
|
||||
self.prompt_prefix_user.encode("utf-8")
|
||||
if self.prompt_prefix_user
|
||||
else None
|
||||
)
|
||||
prompt_suffix_user_c = (
|
||||
self.prompt_suffix_user.encode("utf-8")
|
||||
if self.prompt_suffix_user
|
||||
else None
|
||||
)
|
||||
prompt_prefix_model_c = (
|
||||
self.prompt_prefix_model.encode("utf-8")
|
||||
if self.prompt_prefix_model
|
||||
else None
|
||||
)
|
||||
prompt_suffix_model_c = (
|
||||
self.prompt_suffix_model.encode("utf-8")
|
||||
if self.prompt_suffix_model
|
||||
else None
|
||||
)
|
||||
prompt_prefix_system_c = (
|
||||
self.prompt_prefix_system.encode("utf-8")
|
||||
if self.prompt_prefix_system
|
||||
else None
|
||||
)
|
||||
prompt_suffix_system_c = (
|
||||
self.prompt_suffix_system.encode("utf-8")
|
||||
if self.prompt_suffix_system
|
||||
else None
|
||||
)
|
||||
user_role_token_c = (
|
||||
self.user_role_token.encode("utf-8") if self.user_role_token else None
|
||||
)
|
||||
system_role_token_c = (
|
||||
self.system_role_token.encode("utf-8")
|
||||
if self.system_role_token
|
||||
else None
|
||||
)
|
||||
model_role_token_c = (
|
||||
self.model_role_token.encode("utf-8") if self.model_role_token else None
|
||||
)
|
||||
end_role_token_c = (
|
||||
self.end_role_token.encode("utf-8") if self.end_role_token else None
|
||||
)
|
||||
return llm_bundler_metadata_options_c.LlmBundlerMetadataOptionsC(
|
||||
start_token=start_token_c,
|
||||
stop_tokens=stop_tokens_c,
|
||||
num_stop_tokens=len(self.stop_tokens),
|
||||
enable_bytes_to_unicode_mapping=self.enable_bytes_to_unicode_mapping,
|
||||
system_prompt=system_prompt_c,
|
||||
prompt_prefix_user=prompt_prefix_user_c,
|
||||
prompt_suffix_user=prompt_suffix_user_c,
|
||||
prompt_prefix_model=prompt_prefix_model_c,
|
||||
prompt_suffix_model=prompt_suffix_model_c,
|
||||
prompt_prefix_system=prompt_prefix_system_c,
|
||||
prompt_suffix_system=prompt_suffix_system_c,
|
||||
user_role_token=user_role_token_c,
|
||||
system_role_token=system_role_token_c,
|
||||
model_role_token=model_role_token_c,
|
||||
end_role_token=end_role_token_c,
|
||||
)
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# Copyright 2026 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Ctypes wrapper for LlmBundlerMetadataOptions."""
|
||||
|
||||
import ctypes
|
||||
|
||||
|
||||
class LlmBundlerMetadataOptionsC(ctypes.Structure):
|
||||
"""Wrapper for LlmBundlerMetadataOptions in C."""
|
||||
|
||||
_fields_ = [
|
||||
("start_token", ctypes.c_char_p),
|
||||
("stop_tokens", ctypes.POINTER(ctypes.c_char_p)),
|
||||
("num_stop_tokens", ctypes.c_int),
|
||||
("enable_bytes_to_unicode_mapping", ctypes.c_bool),
|
||||
("system_prompt", ctypes.c_char_p),
|
||||
("prompt_prefix_user", ctypes.c_char_p),
|
||||
("prompt_suffix_user", ctypes.c_char_p),
|
||||
("prompt_prefix_model", ctypes.c_char_p),
|
||||
("prompt_suffix_model", ctypes.c_char_p),
|
||||
("prompt_prefix_system", ctypes.c_char_p),
|
||||
("prompt_suffix_system", ctypes.c_char_p),
|
||||
("user_role_token", ctypes.c_char_p),
|
||||
("system_role_token", ctypes.c_char_p),
|
||||
("model_role_token", ctypes.c_char_p),
|
||||
("end_role_token", ctypes.c_char_p),
|
||||
]
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
# Copyright 2026 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from absl.testing import absltest
|
||||
from mediapipe.tasks.python.genai.bundler import llm_bundler_metadata_options
|
||||
|
||||
|
||||
class LlmBundlerMetadataOptionsTest(absltest.TestCase):
|
||||
|
||||
def test_to_ctypes(self):
|
||||
options = llm_bundler_metadata_options.LlmBundlerMetadataOptions(
|
||||
start_token="start",
|
||||
stop_tokens=["stop1", "stop2"],
|
||||
enable_bytes_to_unicode_mapping=True,
|
||||
system_prompt="system",
|
||||
prompt_prefix_user="user_prefix",
|
||||
prompt_suffix_user="user_suffix",
|
||||
prompt_prefix_model="model_prefix",
|
||||
prompt_suffix_model="model_suffix",
|
||||
prompt_prefix_system=None,
|
||||
prompt_suffix_system=None,
|
||||
user_role_token="user_role",
|
||||
system_role_token=None,
|
||||
model_role_token="model_role",
|
||||
end_role_token="end_role",
|
||||
)
|
||||
|
||||
options_c = options.to_ctypes()
|
||||
|
||||
self.assertEqual(options_c.start_token, b"start")
|
||||
self.assertEqual(options_c.stop_tokens[0], b"stop1")
|
||||
self.assertEqual(options_c.stop_tokens[1], b"stop2")
|
||||
self.assertEqual(options_c.num_stop_tokens, 2)
|
||||
self.assertTrue(options_c.enable_bytes_to_unicode_mapping)
|
||||
self.assertEqual(options_c.system_prompt, b"system")
|
||||
self.assertEqual(options_c.prompt_prefix_user, b"user_prefix")
|
||||
self.assertEqual(options_c.prompt_suffix_user, b"user_suffix")
|
||||
self.assertEqual(options_c.prompt_prefix_model, b"model_prefix")
|
||||
self.assertEqual(options_c.prompt_suffix_model, b"model_suffix")
|
||||
self.assertIsNone(options_c.prompt_prefix_system)
|
||||
self.assertIsNone(options_c.prompt_suffix_system)
|
||||
self.assertEqual(options_c.user_role_token, b"user_role")
|
||||
self.assertIsNone(options_c.system_role_token)
|
||||
self.assertEqual(options_c.model_role_token, b"model_role")
|
||||
self.assertEqual(options_c.end_role_token, b"end_role")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
absltest.main()
|
||||
@@ -0,0 +1,24 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""MediaPipe Tasks GenAI Converter API."""
|
||||
|
||||
import mediapipe.tasks.python.genai.converter.llm_converter
|
||||
|
||||
ConversionConfig = llm_converter.ConversionConfig
|
||||
convert_checkpoint = llm_converter.convert_checkpoint
|
||||
|
||||
# Remove unnecessary modules to avoid duplication in API docs.
|
||||
del mediapipe
|
||||
del llm_converter
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
# Copyright 2024 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Defines a couple base classes for the conversion/quantization process."""
|
||||
|
||||
from typing import Iterator
|
||||
import os
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import numpy as np
|
||||
|
||||
|
||||
class QuantizationAction:
|
||||
"""Container of the tensor values and its corresponding quantization settings.
|
||||
|
||||
The container is responsible for hosting all of the information that is
|
||||
required to execute the weight-only quantization.
|
||||
|
||||
Attributes:
|
||||
tensor_name: A string that represents the input tensor name.
|
||||
tensor_value: A numpy array that contains the unquantized tensor values.
|
||||
target_name: A string that represents the updated tensor name.
|
||||
quantize_axis: A list of integers representing the dimensions to be
|
||||
quantized along. For example, if an input tensor has shape [128, 256] and
|
||||
the quantize_axis==[0], it means the quantization happens along the 0-th
|
||||
dimension, resulting in [256] scaling factors.
|
||||
quantize_bits: An integer that specifies the target quantization bits. It
|
||||
currently only supports either 8 or 4 bits.
|
||||
pack_dim: An integer specifying which dimension to pack the quantized bits.
|
||||
This is only applicable when the quantize_bits == 4.
|
||||
is_quantized: A boolean indicating whether the tensor is already quantized
|
||||
in the checkpoint. Default to False.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tensor_name: str,
|
||||
tensor_value: Optional[np.ndarray] = None,
|
||||
target_name: Optional[str] = None,
|
||||
quantize_axis: Optional[List[int]] = None,
|
||||
quantize_bits: Optional[int] = None,
|
||||
pack_dim: Optional[int] = 0,
|
||||
is_quantized: bool = False,
|
||||
):
|
||||
"""Initializes the model attributes."""
|
||||
self.tensor_name = tensor_name
|
||||
self.tensor_value = tensor_value
|
||||
self.target_name = target_name
|
||||
self.quantize_axis = quantize_axis
|
||||
self.quantize_bits = quantize_bits
|
||||
self.pack_dim = pack_dim
|
||||
self.is_quantized = is_quantized
|
||||
|
||||
def __str__(self) -> str:
|
||||
output_string = "QuantizationAction(\n"
|
||||
output_string += f" tensor_name: {self.tensor_name}\n"
|
||||
output_string += f" target_name: {self.target_name}\n"
|
||||
output_string += f" quantize_axis: {self.quantize_axis}\n"
|
||||
output_string += f" quantize_bits: {self.quantize_bits}\n"
|
||||
output_string += f" pack_dim: {self.pack_dim}\n"
|
||||
output_string += f" is_quantized: {self.is_quantized}\n"
|
||||
if self.tensor_value is not None:
|
||||
output_string += f" tensor_value: {self.tensor_value.shape}\n"
|
||||
output_string += ")\n"
|
||||
return output_string
|
||||
|
||||
|
||||
class CkptLoaderBase:
|
||||
"""Base class for loading the checkpoint.
|
||||
|
||||
This class is responsible for loading the checkpoint files into the layer
|
||||
weight tensors (as numpy arrays) + quantization setting information (8/4
|
||||
bits). The returned data should be a list of QuantizationAction that describes
|
||||
how to quantize each layer weights.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ckpt_path: str,
|
||||
is_symmetric: bool,
|
||||
attention_quant_bits: int,
|
||||
feedforward_quant_bits: int,
|
||||
embedding_quant_bits: int,
|
||||
):
|
||||
"""Initializes the loader.
|
||||
|
||||
Args:
|
||||
ckpt_path: The filepath to the checkpoint.
|
||||
is_symmetric: Whether to apply symmetric or asymmetric quantization.
|
||||
attention_quant_bits: An integer that specify the target quantization bits
|
||||
(support 8 or 4) for the attention layers.
|
||||
feedforward_quant_bits: An integer that specify the target quantization
|
||||
bits (support 8 or 4) for the feedforward layers in each Transformer
|
||||
blocks.
|
||||
embedding_quant_bits: An integer that specify the target quantization bits
|
||||
(support 8 or 4) for the embedding (and the final projection) layers.
|
||||
"""
|
||||
self._ckpt_path = ckpt_path
|
||||
self._is_symmetric = is_symmetric
|
||||
self._attention_quant_bits = attention_quant_bits
|
||||
self._feedforward_quant_bits = feedforward_quant_bits
|
||||
self._embedding_quant_bits = embedding_quant_bits
|
||||
|
||||
def load_to_actions(
|
||||
self,
|
||||
) -> Iterator[Optional[List[QuantizationAction]]]:
|
||||
"""Loads the checkpoint and returns the quantization actions."""
|
||||
raise NotImplementedError("The load_to_actions method is not implemented.")
|
||||
|
||||
|
||||
class LayerActionMapperBase:
|
||||
"""Base class for mapping the layer weights to quantization actions.
|
||||
|
||||
This class is responsible for mapping from each layer to its corresponding
|
||||
quantization information (e.g. target quantization bits / updated tensor
|
||||
name...).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
is_symmetric: bool,
|
||||
attention_quant_bits: int,
|
||||
feedforward_quant_bits: int,
|
||||
embedding_quant_bits: int,
|
||||
backend: str,
|
||||
):
|
||||
self._is_symmetric = is_symmetric
|
||||
self._attention_quant_bits = attention_quant_bits
|
||||
self._feedforward_quant_bits = feedforward_quant_bits
|
||||
self._embedding_quant_bits = embedding_quant_bits
|
||||
self._backend = backend
|
||||
|
||||
def map_to_actions(
|
||||
self, layer_name: str
|
||||
) -> Optional[List[QuantizationAction]]:
|
||||
"""Maps the layer weights to quantization actions.
|
||||
|
||||
Args:
|
||||
layer_name: A string representing the name of the layer weight. Note that
|
||||
it is expected the layer information is contained in the name which is
|
||||
enough to determine the target quantization information. Any child class
|
||||
is expected to implement this function.
|
||||
"""
|
||||
raise NotImplementedError("The map_to_actions method is not implemented.")
|
||||
|
||||
|
||||
class ModelWriterBase:
|
||||
"""Base class for writing the quantized model.
|
||||
|
||||
This class is responsible for taking a dictionary of the quantized
|
||||
tensors/names and writing them into the format that can be loaded by the
|
||||
on-device inference engine.
|
||||
"""
|
||||
|
||||
def __init__(self, output_dir: str, backend: str):
|
||||
"""Initializes the class.
|
||||
|
||||
Args:
|
||||
output_dir: A string that represents the output directory to write the
|
||||
resulting file(s).
|
||||
backend: A string that represents the target backend to run the output
|
||||
file(s).
|
||||
"""
|
||||
self._output_dir = output_dir
|
||||
if not os.path.exists(self._output_dir):
|
||||
os.mkdir(self._output_dir)
|
||||
self._backend = backend
|
||||
|
||||
def write_variables(
|
||||
self,
|
||||
variables: Dict[str, Tuple[np.ndarray, bool]],
|
||||
use_fake_values: bool = False,
|
||||
):
|
||||
raise NotImplementedError("The write_variables method is not implemented.")
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
# Copyright 2024 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Utility library that helps create the converter instances."""
|
||||
from mediapipe.tasks.python.genai.converter import converter_base
|
||||
from mediapipe.tasks.python.genai.converter import pytorch_converter
|
||||
from mediapipe.tasks.python.genai.converter import safetensors_converter
|
||||
from mediapipe.tasks.python.genai.converter import weight_bins_writer
|
||||
|
||||
|
||||
def create_ckpt_loader(
|
||||
ckpt_format: str, *args, **kwargs
|
||||
) -> converter_base.CkptLoaderBase:
|
||||
"""Creates the checkpoint loader.
|
||||
|
||||
Args:
|
||||
ckpt_format: A string that indicates which input checkpoint format is.
|
||||
*args: Additional arguments to be passed into the loader.
|
||||
**kwargs: Additional arguments to be passed into the loader.
|
||||
|
||||
Returns:
|
||||
A created CkptLoader instance.
|
||||
"""
|
||||
del args
|
||||
if ckpt_format == "pytorch":
|
||||
return pytorch_converter.PytorchCkptLoader(
|
||||
ckpt_path=kwargs["ckpt_path"],
|
||||
is_symmetric=kwargs["is_symmetric"],
|
||||
attention_quant_bits=kwargs["attention_quant_bits"],
|
||||
feedforward_quant_bits=kwargs["feedforward_quant_bits"],
|
||||
embedding_quant_bits=kwargs["embedding_quant_bits"],
|
||||
special_model=kwargs["special_model"],
|
||||
backend=kwargs["backend"],
|
||||
)
|
||||
elif ckpt_format == "safetensors":
|
||||
return safetensors_converter.SafetensorsCkptLoader(
|
||||
ckpt_path=kwargs["ckpt_path"],
|
||||
is_symmetric=kwargs["is_symmetric"],
|
||||
attention_quant_bits=kwargs["attention_quant_bits"],
|
||||
feedforward_quant_bits=kwargs["feedforward_quant_bits"],
|
||||
embedding_quant_bits=kwargs["embedding_quant_bits"],
|
||||
special_model=kwargs["special_model"],
|
||||
backend=kwargs["backend"],
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unknown checkpoint format: {ckpt_format}")
|
||||
|
||||
|
||||
def create_writer(
|
||||
writer_type: str, *args, **kwargs
|
||||
) -> converter_base.ModelWriterBase:
|
||||
"""Creates the model writer.
|
||||
|
||||
Args:
|
||||
writer_type: A string the indicates which model writer to create.
|
||||
*args: Additional arguments to be passed into the loader.
|
||||
**kwargs: Additional arguments to be passed into the loader.
|
||||
|
||||
Returns:
|
||||
A created ModelWriter instance.
|
||||
"""
|
||||
del args
|
||||
if writer_type == "weight_bins":
|
||||
return weight_bins_writer.WeightBinsWriter(
|
||||
output_dir=kwargs["output_dir"], backend=kwargs["backend"]
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unknown writer type: {writer_type}")
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
# Copyright 2025 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""MediaPipe LLM Converter Dependencies."""
|
||||
try:
|
||||
import jax
|
||||
from jax import lax
|
||||
from jax import numpy as jnp
|
||||
import torch
|
||||
|
||||
import sentencepiece
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"JAX, Torch, and SentencePiece are required for the LLM conversions."
|
||||
" Please run `pip install torch jax sensetencepiece`."
|
||||
) from e
|
||||
+644
@@ -0,0 +1,644 @@
|
||||
# Copyright 2024 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Functions to perform the checkpoint conversion."""
|
||||
|
||||
import contextlib
|
||||
import ctypes
|
||||
import os
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from absl import logging
|
||||
import numpy as np
|
||||
|
||||
from mediapipe.tasks.python.core import mediapipe_c_bindings
|
||||
from mediapipe.tasks.python.core import mediapipe_c_utils
|
||||
from mediapipe.tasks.python.core import serial_dispatcher
|
||||
from mediapipe.tasks.python.genai.converter import converter_base
|
||||
from mediapipe.tasks.python.genai.converter import converter_factory
|
||||
from mediapipe.tasks.python.genai.converter import external_dependencies
|
||||
from mediapipe.tasks.python.genai.converter import quantization_util
|
||||
|
||||
jnp = external_dependencies.jnp
|
||||
|
||||
_CTYPES_SIGNATURES = (
|
||||
mediapipe_c_utils.CStatusFunction(
|
||||
func_name='MpLlmConverterGenerateCpuTfLite',
|
||||
core_argtypes=(
|
||||
ctypes.c_char_p, # model_type
|
||||
ctypes.c_char_p, # weight_path
|
||||
ctypes.c_char_p, # vocab_model_file
|
||||
ctypes.c_bool, # is_quantized
|
||||
ctypes.c_char_p, # output_tflite_file
|
||||
),
|
||||
),
|
||||
mediapipe_c_utils.CStatusFunction(
|
||||
func_name='MpLlmConverterGenerateGpuTfLite',
|
||||
core_argtypes=(
|
||||
ctypes.c_char_p, # model_type
|
||||
ctypes.c_char_p, # weight_path
|
||||
ctypes.c_char_p, # vocab_model_file
|
||||
ctypes.c_bool, # is_quantized
|
||||
ctypes.c_bool, # obfuscate
|
||||
ctypes.c_char_p, # output_tflite_file
|
||||
ctypes.c_int, # lora_rank
|
||||
ctypes.c_char_p, # lora_weight_path
|
||||
ctypes.c_char_p, # lora_output_tflite_file
|
||||
ctypes.c_char_p, # lora_main_model_type
|
||||
ctypes.c_char_p, # image_encoder_file
|
||||
ctypes.c_char_p, # image_adapter_file
|
||||
ctypes.c_char_p, # submodel_type
|
||||
ctypes.c_bool, # use_dynamic_ple
|
||||
ctypes.c_bool, # apply_srq
|
||||
ctypes.c_int, # block_size
|
||||
),
|
||||
),
|
||||
mediapipe_c_utils.CStatusFunction(
|
||||
func_name='MpLlmConverterConvertHfTokenizer',
|
||||
core_argtypes=(
|
||||
ctypes.c_char_p, # vocab_model_file
|
||||
ctypes.c_char_p, # output_vocab_file
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _safe_encode_str(value: str | None) -> bytes:
|
||||
"""Encodes a string to bytes, returning an empty byte string if None."""
|
||||
return value.encode('utf-8') if value is not None else b''
|
||||
|
||||
|
||||
class ConversionConfig(object):
|
||||
"""Config for checkpoint conversion.
|
||||
|
||||
Attributes:
|
||||
input_ckpt: Directory or path for the input checkpoint.
|
||||
ckpt_format: Checkpoint format, e.g. 'safetensors', 'pytorch'.
|
||||
model_type: Name of the model, e.g. GEMMA_2B.
|
||||
backend: Target backend to run the model. Can be either "cpu" or "gpu".
|
||||
output_dir: Where the output file(s) to be stored.
|
||||
is_quantized: Whether the checkpoint is already quantized. If the checkpoint
|
||||
is already quantized, the converter will not quantize it again and it will
|
||||
ignore the quantization parameters.
|
||||
is_symmetric: Whether to quantize symmetrically.
|
||||
attention_quant_bits: Target quantization bits for the attention layers.
|
||||
feedforward_quant_bits: Target quantization bits for the feedforward layers.
|
||||
embedding_quant_bits: Target quantization bits for the embedding layers.
|
||||
combine_file_only: Whether to combine the weight files only (assuming the
|
||||
weight files are already existed).
|
||||
vocab_model_file: The file path to the 1) SentencePiece vocab model; 2)
|
||||
Hugging Face BPE tokenizer files; 1) is applicable for the Gemma model and
|
||||
2) is applicable for other models. When 2) is used, the provided path is
|
||||
expected to point to a directory that contains both tokenizer.json and
|
||||
tokenizer_config.json files.
|
||||
obfuscate: Whether to obfuscate the model.
|
||||
output_tflite_file: (optional) the output tflite filename. If not provided,
|
||||
the output will be `model.tflite` stored in the output_dir.
|
||||
fp16_scale: A scalar value between [0, 1]. Some models can run into
|
||||
activation overflow issue when running in 16-bit floating point mode. To
|
||||
solve this, we need to scale down the weights of certain layers. See
|
||||
go/llm-on-device-fp16 for more detailed explanation.
|
||||
lora_ckpt: The directory or path for the lora checkpoint. Required in order
|
||||
to convert the lora weights.
|
||||
lora_rank: An integer representing the rank of LoRA. Required in order to
|
||||
convert the lora weights.If not provided, then the converter assumes there
|
||||
is no LoRA weights. Note that only the GPU backend supports LoRA.
|
||||
lora_alpha: A float representing the scale of LoRA weights.
|
||||
lora_output_tflite_file: A string indicating the name of the generated
|
||||
tflite file for the LoRA weight. Only applicable when the lora_rank is not
|
||||
zero.
|
||||
lora_main_model_type: If the LoRA will be applied to a submodel packaged
|
||||
with a main model, what the main model type is.
|
||||
image_encoder_file: A string with the name of the image encoder tflite file.
|
||||
image_adapter_file: A string with the name of the image adapter tflite file.
|
||||
submodel_type: Name of submodel, e.g. GEMMA_2B.
|
||||
use_fake_weights: Whether to use fake weights. If set to True, the weights
|
||||
will be filled with zeros.
|
||||
use_dynamic_ple: Whether any PLE embeddings should be loaded dynamically.
|
||||
Default is true, which will cause embeddings to only be loaded into VRAM
|
||||
on demand.
|
||||
use_mse_quant: Whether to use MSE quantization for recomputing scales.
|
||||
block_size: Default of 0 is off. Can also be 32 or 128, for using blockwise
|
||||
Q4_0 compression on the models and layers which support this.
|
||||
apply_srq: Default of None will apply SRQ for pre-quantized models only.
|
||||
Will override this behavior if set explicitly.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_ckpt: str,
|
||||
ckpt_format: str,
|
||||
model_type: str,
|
||||
backend: str,
|
||||
output_dir: str,
|
||||
is_quantized: bool = False,
|
||||
is_symmetric: bool = True,
|
||||
attention_quant_bits: int = 8,
|
||||
feedforward_quant_bits: int = 8,
|
||||
embedding_quant_bits: int = 8,
|
||||
combine_file_only: bool = False,
|
||||
vocab_model_file: str = '',
|
||||
obfuscate: bool = False,
|
||||
output_tflite_file: Optional[str] = None,
|
||||
fp16_scale: Optional[float] = None,
|
||||
block_size: int = 0,
|
||||
lora_ckpt: Optional[str] = None,
|
||||
lora_rank: Optional[int] = None,
|
||||
lora_alpha: Optional[float] = None,
|
||||
lora_output_tflite_file: Optional[str] = None,
|
||||
lora_main_model_type: Optional[str] = None,
|
||||
image_encoder_file: Optional[str] = None,
|
||||
image_adapter_file: Optional[str] = None,
|
||||
submodel_type: Optional[str] = None,
|
||||
use_fake_weights: bool = False,
|
||||
use_dynamic_ple: bool = True,
|
||||
use_mse_quant: bool = False,
|
||||
apply_srq: Optional[bool] = None,
|
||||
):
|
||||
self.input_ckpt = input_ckpt
|
||||
self.ckpt_format = ckpt_format
|
||||
self.model_type = model_type
|
||||
self.backend = backend
|
||||
if os.path.isfile(output_dir):
|
||||
raise ValueError('Output directory mush not point to an existing file.')
|
||||
if not os.path.isdir(output_dir):
|
||||
logging.info('Creating output directory: %s', output_dir)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
self.output_dir = output_dir
|
||||
self.is_quantized = is_quantized
|
||||
self.is_symmetric = is_symmetric
|
||||
self.attention_quant_bits = attention_quant_bits
|
||||
self.feedforward_quant_bits = feedforward_quant_bits
|
||||
self.embedding_quant_bits = embedding_quant_bits
|
||||
self.combine_file_only = combine_file_only
|
||||
self.vocab_model_file = vocab_model_file
|
||||
self.obfuscate = obfuscate
|
||||
self.image_encoder_file = image_encoder_file
|
||||
self.image_adapter_file = image_adapter_file
|
||||
self.submodel_type = submodel_type
|
||||
self.use_fake_weights = use_fake_weights
|
||||
self.use_dynamic_ple = use_dynamic_ple
|
||||
self.use_mse_quant = use_mse_quant
|
||||
self.apply_srq = apply_srq
|
||||
if output_tflite_file:
|
||||
parent_dir = os.path.dirname(output_tflite_file)
|
||||
if not os.path.isdir(parent_dir):
|
||||
logging.info('Creating tflite parent directory: %s', parent_dir)
|
||||
os.makedirs(parent_dir, exist_ok=True)
|
||||
self.output_tflite_file = output_tflite_file
|
||||
else:
|
||||
self.output_tflite_file = os.path.join(output_dir, 'model.tflite')
|
||||
|
||||
self.fp16_scale = None
|
||||
if block_size not in (0, 32, 128):
|
||||
raise ValueError('Block size must be one of (0 (off), 32, 128)')
|
||||
else:
|
||||
self.block_size = block_size
|
||||
self.lora_ckpt = lora_ckpt
|
||||
self.lora_rank = lora_rank
|
||||
self.lora_alpha = lora_alpha
|
||||
self.lora_output_tflite_file = lora_output_tflite_file
|
||||
self.lora_main_model_type = lora_main_model_type
|
||||
if (self.lora_ckpt is None) ^ (self.lora_rank is None):
|
||||
raise ValueError(
|
||||
'lora_ckpt and lora_rank must be either both provided or both not'
|
||||
' provided.'
|
||||
)
|
||||
if self.lora_rank is not None:
|
||||
if backend == 'cpu':
|
||||
raise ValueError('LoRA is not supported for CPU backend.')
|
||||
lora_applicable_models = [
|
||||
'GEMMA_2B',
|
||||
'GEMMA2_2B',
|
||||
'PHI_2',
|
||||
'GEMMA3_1B',
|
||||
'GEMMA3_4B',
|
||||
'GEMMA3_12B',
|
||||
'GEMMA3_27B',
|
||||
'GEMMA3_300M',
|
||||
]
|
||||
if model_type not in lora_applicable_models:
|
||||
raise ValueError(
|
||||
'LoRA is only applicable for the model_type:'
|
||||
f' {", ".join(lora_applicable_models)}, but get model_type:'
|
||||
f' {model_type}.'
|
||||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def filemanager(filename: str, mode: str):
|
||||
try:
|
||||
with open(filename, mode) as f:
|
||||
yield f
|
||||
finally:
|
||||
pass
|
||||
|
||||
|
||||
class _LlmConverter:
|
||||
"""Bundles all conversion logic for LLM models."""
|
||||
|
||||
def __init__(self, lib: Any):
|
||||
self._lib = serial_dispatcher.SerialDispatcher(lib, _CTYPES_SIGNATURES)
|
||||
|
||||
def quantize_by_actions(
|
||||
self,
|
||||
actions: List[converter_base.QuantizationAction],
|
||||
backend: str,
|
||||
is_symmetric: bool,
|
||||
use_mse_quant: bool = False,
|
||||
block_size: int = 0,
|
||||
):
|
||||
"""Quantizes the weights by actions.
|
||||
|
||||
Args:
|
||||
actions: A list of QuantizationAction that contains the information and
|
||||
tensor values to be quantized.
|
||||
backend: Target backend to run the model. Can be either "cpu" or "gpu".
|
||||
is_symmetric: Whether to quantize symmetrically.
|
||||
use_mse_quant: Whether to use MSE quantization for recomputing scales.
|
||||
block_size: 0 to disable, and 32 or 128 to use blockwise Q4_0 for
|
||||
recomputing scales.
|
||||
|
||||
Returns:
|
||||
A dictionary that maps from the updated tensor names to the quantized
|
||||
tensor values + a boolean that indicates whether the tensor values need to
|
||||
be packed (only applicable for the 4-bit quantized weights).
|
||||
"""
|
||||
output_tensors = {}
|
||||
qvalue_suffix = '_quantized_value'
|
||||
scale_suffix = '_quantized_scale'
|
||||
zp_suffix = '_quantized_zp'
|
||||
for action in actions:
|
||||
if action.tensor_value is None:
|
||||
continue
|
||||
# The dtype needs to be compared in string as it is a custom numpy dtype.
|
||||
# Explicitly cast the bfloat16 and float16 dtype to float32 to make sure
|
||||
# its value is converted and serialized correctly.
|
||||
if (
|
||||
str(action.tensor_value.dtype) == 'bfloat16'
|
||||
or action.tensor_value.dtype == np.float16
|
||||
):
|
||||
action.tensor_value = action.tensor_value.astype(np.float32)
|
||||
if (
|
||||
(not action.is_quantized)
|
||||
and action.tensor_value.dtype != np.float32
|
||||
and action.tensor_value.dtype != np.int8
|
||||
):
|
||||
raise ValueError(
|
||||
'All tensors should be casted to either float32 or int8, but got: '
|
||||
'%s'
|
||||
% action.tensor_value.dtype
|
||||
)
|
||||
if action.is_quantized:
|
||||
pack = action.tensor_value.dtype == jnp.int4
|
||||
if qvalue_suffix in action.target_name:
|
||||
target_name = action.target_name[: -len(qvalue_suffix)]
|
||||
# Stores the quantized value in int8 for 4-bit quantization.
|
||||
if pack:
|
||||
action.tensor_value = action.tensor_value.astype(jnp.int8)
|
||||
output_tensors[target_name] = (action.tensor_value, pack)
|
||||
elif (
|
||||
scale_suffix in action.target_name
|
||||
or zp_suffix in action.target_name
|
||||
):
|
||||
output_tensors[action.target_name] = (
|
||||
action.tensor_value,
|
||||
False,
|
||||
)
|
||||
else:
|
||||
output_tensors[action.target_name] = (action.tensor_value, False)
|
||||
if action.quantize_axis:
|
||||
quant_bits = action.quantize_bits
|
||||
quant_axis = action.quantize_axis.copy()
|
||||
if (
|
||||
block_size > 0
|
||||
and quant_bits == 4
|
||||
and quant_axis != [0]
|
||||
and quant_axis != [1]
|
||||
and quant_axis != [0, 1]
|
||||
):
|
||||
# Turn off 4bit blockwise quant for unsupported quantization axis
|
||||
# configurations
|
||||
print(
|
||||
'Disabling blockwise 4bit quant for unsupported quantization'
|
||||
' axis: ',
|
||||
quant_axis,
|
||||
' on tensor ',
|
||||
action.target_name,
|
||||
)
|
||||
quant_bits = 8
|
||||
pack = quant_bits == 4
|
||||
|
||||
if action.tensor_value.dtype == np.int8:
|
||||
if backend == 'cpu' and pack:
|
||||
raise ValueError(
|
||||
'Converting pre-quantized checkpoint into 4-bit is not'
|
||||
' supported for CPU backend.'
|
||||
)
|
||||
output_tensors[action.target_name] = (action.tensor_value, pack)
|
||||
else:
|
||||
if is_symmetric:
|
||||
bs = block_size if pack else 0
|
||||
target_var, scale = quantization_util.quantize_tensor(
|
||||
var=action.tensor_value,
|
||||
axis=action.quantize_axis,
|
||||
sym=is_symmetric,
|
||||
number_bits=quant_bits,
|
||||
use_mse_quant=use_mse_quant,
|
||||
# blockwise quant applies to symmetric 4bit only
|
||||
block_size=bs,
|
||||
)
|
||||
if bs > 0:
|
||||
if len(scale.shape) > 3:
|
||||
raise ValueError(
|
||||
'Blockwise int4 quantization not yet supported for'
|
||||
' 4-dimensional or higher scales.'
|
||||
)
|
||||
# Reshape and transpose output from blockwise quant.
|
||||
# We want per-block scale values as the last dimension.
|
||||
if quant_axis == [0] or quant_axis == [0, 1]:
|
||||
if len(scale.shape) <= 2:
|
||||
scale = scale.transpose()
|
||||
else:
|
||||
scale = scale.transpose([1, 2, 0])
|
||||
elif quant_axis == [1]:
|
||||
if len(scale.shape) == 3:
|
||||
# Only tested in one case, so not sure if this generalizes.
|
||||
# Seems to behave identically to our experimental
|
||||
# quant_axis = [0, 1] support so far.
|
||||
scale = scale.transpose([2, 0, 1])
|
||||
|
||||
# Reshape to fold in the per-block dimension
|
||||
target_var = target_var.reshape(action.tensor_value.shape)
|
||||
output_tensors[action.target_name] = (target_var, pack)
|
||||
output_tensors[action.target_name + scale_suffix] = (
|
||||
scale,
|
||||
False,
|
||||
)
|
||||
zp = None
|
||||
else:
|
||||
target_var, scale, zp = quantization_util.quantize_tensor(
|
||||
var=action.tensor_value,
|
||||
axis=action.quantize_axis,
|
||||
sym=is_symmetric,
|
||||
number_bits=action.quantize_bits,
|
||||
use_mse_quant=use_mse_quant,
|
||||
)
|
||||
if backend == 'cpu' and pack:
|
||||
target_var, scale, zp = quantization_util.update_to_uint4(
|
||||
target_var, scale, zp
|
||||
)
|
||||
output_tensors[action.target_name] = (target_var, pack)
|
||||
output_tensors[action.target_name + scale_suffix] = (scale, False)
|
||||
if zp is not None:
|
||||
output_tensors[action.target_name + zp_suffix] = (zp, False)
|
||||
else:
|
||||
output_tensors[action.target_name] = (action.tensor_value, False)
|
||||
return output_tensors
|
||||
|
||||
def combined_weight_bins_to_tflite(
|
||||
self,
|
||||
model_type: str,
|
||||
backend: str,
|
||||
weight_path: str,
|
||||
output_tflite_file: str,
|
||||
obfuscate: bool,
|
||||
vocab_model_file: str,
|
||||
lora_rank: Optional[int] = None,
|
||||
lora_weight_path: Optional[str] = None,
|
||||
lora_output_tflite_file: Optional[str] = None,
|
||||
lora_main_model_type: Optional[str] = None,
|
||||
image_encoder_file: Optional[str] = None,
|
||||
image_adapter_file: Optional[str] = None,
|
||||
submodel_type: Optional[str] = None,
|
||||
use_dynamic_ple: Optional[bool] = None,
|
||||
apply_srq: Optional[bool] = None,
|
||||
block_size: int = 0,
|
||||
):
|
||||
"""Combines weight files to tflite file."""
|
||||
if backend == 'cpu':
|
||||
if lora_rank is not None:
|
||||
logging.fatal('LoRA is not supported for CPU backend.')
|
||||
self._lib.MpLlmConverterGenerateCpuTfLite(
|
||||
model_type.encode('utf-8'),
|
||||
weight_path.encode('utf-8'),
|
||||
vocab_model_file.encode('utf-8'),
|
||||
True,
|
||||
output_tflite_file.encode('utf-8'),
|
||||
)
|
||||
elif backend == 'gpu':
|
||||
self._lib.MpLlmConverterGenerateGpuTfLite(
|
||||
model_type.encode('utf-8'),
|
||||
weight_path.encode('utf-8'),
|
||||
vocab_model_file.encode('utf-8'),
|
||||
True,
|
||||
obfuscate,
|
||||
output_tflite_file.encode('utf-8'),
|
||||
0 if lora_rank is None else lora_rank,
|
||||
_safe_encode_str(lora_weight_path),
|
||||
_safe_encode_str(lora_output_tflite_file),
|
||||
_safe_encode_str(lora_main_model_type),
|
||||
_safe_encode_str(image_encoder_file),
|
||||
_safe_encode_str(image_adapter_file),
|
||||
_safe_encode_str(submodel_type),
|
||||
True if use_dynamic_ple is None else use_dynamic_ple,
|
||||
False if apply_srq is None else apply_srq,
|
||||
block_size,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f'Unsupported backend: {backend}')
|
||||
|
||||
def convert_bpe_vocab(self, vocab_model_file: str, output_dir: str) -> str:
|
||||
"""Converts the BPE vocab model file to SPM format.
|
||||
|
||||
Args:
|
||||
vocab_model_file: The input BPE vocab model file path.
|
||||
output_dir: The output directory to store the SPM vocab model file.
|
||||
|
||||
Returns:
|
||||
The path to the output SPM vocab model file.
|
||||
"""
|
||||
if not os.path.isdir(vocab_model_file):
|
||||
raise ValueError(
|
||||
'The input BPE vocab model file path is expected to be a directory'
|
||||
' that contains both tokenizer.json and tokenizer_config.json files.'
|
||||
)
|
||||
output_vocab_file = os.path.join(output_dir, 'spm.model')
|
||||
self._lib.MpLlmConverterConvertHfTokenizer(
|
||||
vocab_model_file.encode('utf-8'),
|
||||
output_vocab_file.encode('utf-8'),
|
||||
)
|
||||
return output_vocab_file
|
||||
|
||||
def sort_layer_info(self, layer_info_file: str) -> None:
|
||||
"""Loads and sorts the layer info file."""
|
||||
layer_info = []
|
||||
with filemanager(layer_info_file, 'r') as finfo:
|
||||
for line in finfo:
|
||||
line = line.strip()
|
||||
if line:
|
||||
layer_info.append(line)
|
||||
layer_info = list(set(layer_info))
|
||||
layer_info.sort()
|
||||
with filemanager(layer_info_file, 'w') as finfo:
|
||||
for line in layer_info:
|
||||
finfo.write(line + '\n')
|
||||
finfo.write('\n')
|
||||
|
||||
def maybe_quantize_and_write_tensors_to_bins(
|
||||
self,
|
||||
ckpt_loader: converter_base.CkptLoaderBase,
|
||||
config: ConversionConfig,
|
||||
) -> None:
|
||||
"""Quantizes the weight tensors according to the loader and writes them to bins."""
|
||||
actions = ckpt_loader.load_to_actions()
|
||||
|
||||
for action in actions:
|
||||
# Quantize the weight
|
||||
quantized_tensors = self.quantize_by_actions(
|
||||
action,
|
||||
config.backend,
|
||||
config.is_symmetric,
|
||||
config.use_mse_quant,
|
||||
block_size=config.block_size,
|
||||
)
|
||||
del action
|
||||
# Write the tensors into file(s).
|
||||
writer = converter_factory.create_writer(
|
||||
writer_type='weight_bins',
|
||||
output_dir=config.output_dir,
|
||||
backend=config.backend,
|
||||
)
|
||||
writer.write_variables(quantized_tensors, config.use_fake_weights)
|
||||
del quantized_tensors
|
||||
del writer
|
||||
|
||||
def convert_checkpoint(self, config: ConversionConfig) -> None:
|
||||
"""Converts the checkpoint to tflite file."""
|
||||
logging.info('input folder: %s', config.input_ckpt)
|
||||
|
||||
if os.path.isdir(config.vocab_model_file):
|
||||
vocab_model_path = self.convert_bpe_vocab(
|
||||
config.vocab_model_file, config.output_dir
|
||||
)
|
||||
else:
|
||||
vocab_model_path = config.vocab_model_file
|
||||
|
||||
if not config.combine_file_only:
|
||||
# Load the layer weights and prepare the quantization configurations.
|
||||
loader = converter_factory.create_ckpt_loader(
|
||||
config.ckpt_format,
|
||||
ckpt_path=config.input_ckpt,
|
||||
is_quantized=config.is_quantized,
|
||||
is_symmetric=config.is_symmetric,
|
||||
backend=config.backend,
|
||||
attention_quant_bits=config.attention_quant_bits,
|
||||
feedforward_quant_bits=config.feedforward_quant_bits,
|
||||
embedding_quant_bits=config.embedding_quant_bits,
|
||||
special_model=config.model_type,
|
||||
fp16_scale=config.fp16_scale,
|
||||
)
|
||||
self.maybe_quantize_and_write_tensors_to_bins(loader, config)
|
||||
|
||||
if config.lora_ckpt is not None and config.lora_ckpt != config.input_ckpt:
|
||||
# If lora ckpt and the input ckpt is the same. The lora conversion is
|
||||
# handled in the previous loader.
|
||||
lora_loader = converter_factory.create_ckpt_loader(
|
||||
config.ckpt_format,
|
||||
ckpt_path=config.lora_ckpt,
|
||||
is_quantized=config.is_quantized,
|
||||
is_symmetric=config.is_symmetric,
|
||||
backend=config.backend,
|
||||
attention_quant_bits=config.attention_quant_bits,
|
||||
feedforward_quant_bits=config.feedforward_quant_bits,
|
||||
embedding_quant_bits=config.embedding_quant_bits,
|
||||
special_model=config.model_type,
|
||||
)
|
||||
self.maybe_quantize_and_write_tensors_to_bins(lora_loader, config)
|
||||
|
||||
self.sort_layer_info(os.path.join(config.output_dir, 'layer_info.txt'))
|
||||
|
||||
self.combined_weight_bins_to_tflite(
|
||||
config.model_type,
|
||||
config.backend,
|
||||
weight_path=config.output_dir,
|
||||
output_tflite_file=config.output_tflite_file,
|
||||
obfuscate=config.obfuscate,
|
||||
vocab_model_file=vocab_model_path,
|
||||
lora_rank=config.lora_rank,
|
||||
lora_weight_path=config.output_dir,
|
||||
lora_output_tflite_file=config.lora_output_tflite_file,
|
||||
lora_main_model_type=config.lora_main_model_type,
|
||||
image_encoder_file=config.image_encoder_file,
|
||||
image_adapter_file=config.image_adapter_file,
|
||||
submodel_type=config.submodel_type,
|
||||
use_dynamic_ple=config.use_dynamic_ple,
|
||||
# Default SRQ behavior is to match is_quantized
|
||||
apply_srq=config.is_quantized
|
||||
if config.apply_srq is None
|
||||
else config.apply_srq,
|
||||
block_size=config.block_size,
|
||||
)
|
||||
|
||||
def __enter__(self) -> '_LlmConverter':
|
||||
"""Returns `self` upon entering the runtime context."""
|
||||
return self
|
||||
|
||||
def __exit__(self, *args) -> None:
|
||||
"""Shuts down the LlmConverter on exit of the context manager.
|
||||
|
||||
Args:
|
||||
*args: Unused.
|
||||
Raises:
|
||||
RuntimeError: If the LLM converter failed to close.
|
||||
"""
|
||||
del args # Unused.
|
||||
self._lib.close()
|
||||
|
||||
|
||||
def convert_checkpoint(config: ConversionConfig) -> None:
|
||||
"""Converts the checkpoint to tflite file."""
|
||||
lib = mediapipe_c_bindings.load_raw_library(_CTYPES_SIGNATURES)
|
||||
with _LlmConverter(lib) as converter:
|
||||
converter.convert_checkpoint(config)
|
||||
|
||||
|
||||
def quantize_by_actions(
|
||||
actions: List[converter_base.QuantizationAction],
|
||||
backend: str,
|
||||
is_symmetric: bool,
|
||||
use_mse_quant: bool = False,
|
||||
block_size: int = 0,
|
||||
):
|
||||
"""Quantizes the weights by actions.
|
||||
|
||||
Args:
|
||||
actions: A list of QuantizationAction that contains the information and
|
||||
tensor values to be quantized.
|
||||
backend: Target backend to run the model. Can be either "cpu" or "gpu".
|
||||
is_symmetric: Whether to quantize symmetrically.
|
||||
use_mse_quant: Whether to use MSE quantization for recomputing scales.
|
||||
block_size: 0 to disable, and 32 or 128 to use blockwise quantization for
|
||||
recomputing scales.
|
||||
|
||||
Returns:
|
||||
A dictionary that maps from the updated tensor names to the quantized
|
||||
tensor values + a boolean that indicates whether the tensor values need to
|
||||
be packed (only applicable for the 4-bit quantized weights).
|
||||
"""
|
||||
lib = mediapipe_c_bindings.load_raw_library(_CTYPES_SIGNATURES)
|
||||
with _LlmConverter(lib) as converter:
|
||||
return converter.quantize_by_actions(
|
||||
actions, backend, is_symmetric, use_mse_quant, block_size
|
||||
)
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
# Copyright 2025 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from absl.testing import parameterized
|
||||
import numpy as np
|
||||
import tensorflow as tf
|
||||
|
||||
import unittest
|
||||
from mediapipe.tasks.python.genai.converter import converter_base
|
||||
from mediapipe.tasks.python.genai.converter import llm_converter
|
||||
|
||||
_LLM_CONVERTER_FUNCTIONS = (
|
||||
'MpLlmConverterGenerateCpuTfLite',
|
||||
'MpLlmConverterGenerateGpuTfLite',
|
||||
'MpLlmConverterConvertHfTokenizer',
|
||||
)
|
||||
|
||||
|
||||
class LlmConverterTest(googletest.TestCase, parameterized.TestCase):
|
||||
|
||||
def get_fake_action(self, input_dtype):
|
||||
if input_dtype == 'bfloat16':
|
||||
# Create a TensorFlow bfloat16 tensor
|
||||
bfloat16_tensor = tf.constant([1.0, -1.0, 2.0, -2.0], dtype=tf.bfloat16)
|
||||
# Convert the TensorFlow tensor to a NumPy array
|
||||
tensor_value = bfloat16_tensor.numpy()
|
||||
else:
|
||||
tensor_value = np.array(
|
||||
[1.0, -1.0, 2.0, -2.0], dtype=np.dtype(input_dtype)
|
||||
)
|
||||
return converter_base.QuantizationAction(
|
||||
tensor_name='mdl_vars.params.lm.softmax.logits_ffn.w',
|
||||
target_name='params.lm.softmax.logits_ffn.w',
|
||||
quantize_axis=[0],
|
||||
quantize_bits=8,
|
||||
pack_dim=0,
|
||||
tensor_value=tensor_value,
|
||||
)
|
||||
|
||||
@parameterized.parameters(
|
||||
{'input_dtype': 'float32'},
|
||||
{'input_dtype': 'float16'},
|
||||
{'input_dtype': 'bfloat16'},
|
||||
{'input_dtype': 'int8'},
|
||||
)
|
||||
def test_quantize_by_actions(self, input_dtype):
|
||||
out = llm_converter.quantize_by_actions(
|
||||
[self.get_fake_action(input_dtype)], backend='gpu', is_symmetric=True
|
||||
)
|
||||
|
||||
if input_dtype == 'int8':
|
||||
# The values are pre-quantized and should be the same.
|
||||
np.testing.assert_allclose(
|
||||
out['params.lm.softmax.logits_ffn.w'][0],
|
||||
np.array([1, -1, 2, -2], dtype=np.int8),
|
||||
)
|
||||
else:
|
||||
np.testing.assert_allclose(
|
||||
out['params.lm.softmax.logits_ffn.w'][0],
|
||||
np.array([64, -64, 127, -127], dtype=np.int8),
|
||||
)
|
||||
np.testing.assert_allclose(
|
||||
out['params.lm.softmax.logits_ffn.w_quantized_scale'][0],
|
||||
np.array(0.015748, dtype=np.float32),
|
||||
rtol=1e-03,
|
||||
)
|
||||
|
||||
def test_generate_cpu_tflite_receives_correct_args(self):
|
||||
c_lib = mock.MagicMock(spec_set=_LLM_CONVERTER_FUNCTIONS)
|
||||
c_lib.MpLlmConverterGenerateCpuTfLite.return_value = 0
|
||||
|
||||
config = llm_converter.ConversionConfig(
|
||||
model_type='GEMMA_2B',
|
||||
backend='cpu',
|
||||
input_ckpt='/tmp/ckpt',
|
||||
ckpt_format='fake',
|
||||
output_dir='/tmp/',
|
||||
vocab_model_file='/tmp/vocab.model',
|
||||
output_tflite_file='/tmp/output.tflite',
|
||||
)
|
||||
llm_converter_lib = llm_converter._LlmConverter(c_lib)
|
||||
|
||||
llm_converter_lib.combined_weight_bins_to_tflite(
|
||||
model_type=config.model_type,
|
||||
backend=config.backend,
|
||||
weight_path=config.output_dir,
|
||||
output_tflite_file=config.output_tflite_file,
|
||||
obfuscate=config.obfuscate,
|
||||
vocab_model_file=config.vocab_model_file,
|
||||
)
|
||||
|
||||
c_lib.MpLlmConverterGenerateCpuTfLite.assert_called_once_with(
|
||||
b'GEMMA_2B',
|
||||
b'/tmp/',
|
||||
b'/tmp/vocab.model',
|
||||
True,
|
||||
b'/tmp/output.tflite',
|
||||
mock.ANY, # error_message
|
||||
)
|
||||
|
||||
def test_generate_cpu_tflite_propagates_failure(self):
|
||||
c_lib = mock.MagicMock(spec_set=_LLM_CONVERTER_FUNCTIONS)
|
||||
c_lib.MpLlmConverterGenerateCpuTfLite.return_value = 13 # Simulate failure
|
||||
|
||||
config = llm_converter.ConversionConfig(
|
||||
model_type='GEMMA_2B',
|
||||
backend='cpu',
|
||||
input_ckpt='/tmp/ckpt',
|
||||
ckpt_format='fake',
|
||||
output_dir='/tmp/',
|
||||
vocab_model_file='/tmp/vocab.model',
|
||||
output_tflite_file='/tmp/output.tflite',
|
||||
)
|
||||
llm_converter_lib = llm_converter._LlmConverter(c_lib)
|
||||
|
||||
with self.assertRaises(RuntimeError):
|
||||
llm_converter_lib.combined_weight_bins_to_tflite(
|
||||
model_type=config.model_type,
|
||||
backend=config.backend,
|
||||
weight_path=config.output_dir,
|
||||
output_tflite_file=config.output_tflite_file,
|
||||
obfuscate=config.obfuscate,
|
||||
vocab_model_file=config.vocab_model_file,
|
||||
)
|
||||
|
||||
def test_generate_gpu_tflite_receives_correct_args(self):
|
||||
c_lib = mock.MagicMock(spec_set=_LLM_CONVERTER_FUNCTIONS)
|
||||
c_lib.MpLlmConverterGenerateGpuTfLite.return_value = 0
|
||||
|
||||
config = llm_converter.ConversionConfig(
|
||||
model_type='GEMMA_2B',
|
||||
backend='gpu',
|
||||
input_ckpt='/tmp/ckpt',
|
||||
ckpt_format='fake',
|
||||
output_dir='/tmp/',
|
||||
vocab_model_file='/tmp/vocab.model',
|
||||
output_tflite_file='/tmp/output.tflite',
|
||||
use_dynamic_ple=True,
|
||||
)
|
||||
llm_converter_lib = llm_converter._LlmConverter(c_lib)
|
||||
|
||||
llm_converter_lib.combined_weight_bins_to_tflite(
|
||||
model_type=config.model_type,
|
||||
backend=config.backend,
|
||||
weight_path=config.output_dir,
|
||||
output_tflite_file=config.output_tflite_file,
|
||||
obfuscate=config.obfuscate,
|
||||
vocab_model_file=config.vocab_model_file,
|
||||
lora_rank=4,
|
||||
lora_weight_path='/tmp/lora_weights',
|
||||
lora_output_tflite_file='/tmp/lora_output.tflite',
|
||||
lora_main_model_type='GEMMA_2B',
|
||||
image_encoder_file='/tmp/image_encoder.tflite',
|
||||
image_adapter_file='/tmp/image_adapter.tflite',
|
||||
submodel_type='GEMMA_2B',
|
||||
use_dynamic_ple=config.use_dynamic_ple,
|
||||
apply_srq=False,
|
||||
)
|
||||
|
||||
c_lib.MpLlmConverterGenerateGpuTfLite.assert_called_once_with(
|
||||
b'GEMMA_2B',
|
||||
b'/tmp/',
|
||||
b'/tmp/vocab.model',
|
||||
True,
|
||||
False,
|
||||
b'/tmp/output.tflite',
|
||||
4,
|
||||
b'/tmp/lora_weights',
|
||||
b'/tmp/lora_output.tflite',
|
||||
b'GEMMA_2B',
|
||||
b'/tmp/image_encoder.tflite',
|
||||
b'/tmp/image_adapter.tflite',
|
||||
b'GEMMA_2B',
|
||||
True,
|
||||
False,
|
||||
0,
|
||||
mock.ANY, # error_message
|
||||
)
|
||||
|
||||
def test_generate_gpu_tflite_propagates_failure(self):
|
||||
c_lib = mock.MagicMock(spec_set=_LLM_CONVERTER_FUNCTIONS)
|
||||
c_lib.MpLlmConverterGenerateGpuTfLite.return_value = 13 # Simulate failure
|
||||
|
||||
config = llm_converter.ConversionConfig(
|
||||
model_type='GEMMA_2B',
|
||||
backend='gpu',
|
||||
input_ckpt='/tmp/ckpt',
|
||||
ckpt_format='fake',
|
||||
output_dir='/tmp/',
|
||||
vocab_model_file='/tmp/vocab.model',
|
||||
output_tflite_file='/tmp/output.tflite',
|
||||
use_dynamic_ple=True,
|
||||
)
|
||||
llm_converter_lib = llm_converter._LlmConverter(c_lib)
|
||||
|
||||
with self.assertRaises(RuntimeError):
|
||||
llm_converter_lib.combined_weight_bins_to_tflite(
|
||||
model_type=config.model_type,
|
||||
backend=config.backend,
|
||||
weight_path=config.output_dir,
|
||||
output_tflite_file=config.output_tflite_file,
|
||||
obfuscate=config.obfuscate,
|
||||
vocab_model_file=config.vocab_model_file,
|
||||
lora_rank=4,
|
||||
lora_weight_path='/tmp/lora_weights',
|
||||
lora_output_tflite_file='/tmp/lora_output.tflite',
|
||||
lora_main_model_type='GEMMA_2B',
|
||||
image_encoder_file='/tmp/image_encoder.tflite',
|
||||
image_adapter_file='/tmp/image_adapter.tflite',
|
||||
submodel_type='GEMMA_2B',
|
||||
use_dynamic_ple=config.use_dynamic_ple,
|
||||
apply_srq=False,
|
||||
)
|
||||
|
||||
@mock.patch('os.path.isdir', return_value=True)
|
||||
@mock.patch('os.path.join', return_value='/tmp/spm.model')
|
||||
def test_convert_hf_tokenizer_receives_correct_args(
|
||||
self, mock_join, mock_isdir
|
||||
):
|
||||
del mock_join, mock_isdir
|
||||
|
||||
c_lib = mock.MagicMock(spec_set=_LLM_CONVERTER_FUNCTIONS)
|
||||
c_lib.MpLlmConverterConvertHfTokenizer.return_value = 0
|
||||
|
||||
llm_converter_lib = llm_converter._LlmConverter(c_lib)
|
||||
output_file = llm_converter_lib.convert_bpe_vocab(
|
||||
vocab_model_file='/tmp/hf_tokenizer', output_dir='/tmp'
|
||||
)
|
||||
|
||||
c_lib.MpLlmConverterConvertHfTokenizer.assert_called_once_with(
|
||||
b'/tmp/hf_tokenizer', b'/tmp/spm.model', mock.ANY
|
||||
)
|
||||
self.assertEqual(output_file, '/tmp/spm.model')
|
||||
|
||||
@mock.patch('os.path.isdir', return_value=True)
|
||||
@mock.patch('os.path.join', return_value='/tmp/spm.model')
|
||||
def test_convert_hf_tokenizer_propagates_failure(self, mock_join, mock_isdir):
|
||||
del mock_join, mock_isdir
|
||||
|
||||
c_lib = mock.MagicMock(spec_set=_LLM_CONVERTER_FUNCTIONS)
|
||||
c_lib.MpLlmConverterConvertHfTokenizer.return_value = 13 # Simulate failure
|
||||
|
||||
llm_converter_lib = llm_converter._LlmConverter(c_lib)
|
||||
with self.assertRaises(RuntimeError):
|
||||
llm_converter_lib.convert_bpe_vocab(
|
||||
vocab_model_file='/tmp/hf_tokenizer', output_dir='/tmp'
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
googletest.main()
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
# Copyright 2024 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""CkptLoader implementation for loading the Pytorch file."""
|
||||
|
||||
from typing import Iterator
|
||||
import enum
|
||||
import os
|
||||
from typing import List, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from mediapipe.tasks.python.genai.converter import converter_base
|
||||
from mediapipe.tasks.python.genai.converter import external_dependencies
|
||||
|
||||
torch = external_dependencies.torch
|
||||
|
||||
|
||||
class _PytorchReader:
|
||||
"""Pytorch reader."""
|
||||
|
||||
def __init__(self, model_path: str):
|
||||
if not os.path.exists(model_path):
|
||||
raise ValueError(f"{model_path} does not exists.")
|
||||
self._model = torch.load(model_path, map_location=torch.device("cpu"))
|
||||
|
||||
def read_tensor_as_numpy(self, tensor_name) -> np.ndarray:
|
||||
tensor = (
|
||||
self._model[tensor_name]
|
||||
.to(torch.float32)
|
||||
.t()
|
||||
.contiguous()
|
||||
.detach()
|
||||
.cpu()
|
||||
.numpy()
|
||||
)
|
||||
return tensor
|
||||
|
||||
def get_tensor_names(self) -> List[str]:
|
||||
names = list(self._model.keys())
|
||||
return names
|
||||
|
||||
|
||||
class LayerType(enum.Enum):
|
||||
"""Enum for layer type."""
|
||||
|
||||
NONE = 0
|
||||
ATTENTION = 1 # Layer is part of the attention module.
|
||||
FEEDFORWARD = 2 # Layer is part of the feedforward module in the Transformer.
|
||||
EMBEDDING = 3 # Layer is the embedding lookup or final projection layer.
|
||||
LAYER_NORM = (
|
||||
4 # Layer is layer normalization before and after attention layer.
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_layer_type(cls, layer_name: str):
|
||||
"""Gets the layer type of the given layer name."""
|
||||
ffn_layers = [
|
||||
"mlp",
|
||||
]
|
||||
attn_layers = [
|
||||
"self_attention",
|
||||
]
|
||||
emb_layers = [
|
||||
"word_embeddings",
|
||||
"lm_head",
|
||||
]
|
||||
layer_norms = [
|
||||
"input_layernorm",
|
||||
"post_attention_layernorm",
|
||||
"ln_f",
|
||||
]
|
||||
if any(sub_name in layer_name for sub_name in attn_layers):
|
||||
return LayerType.ATTENTION
|
||||
if any(sub_name in layer_name for sub_name in ffn_layers):
|
||||
return LayerType.FEEDFORWARD
|
||||
if any(sub_name in layer_name for sub_name in emb_layers):
|
||||
return LayerType.EMBEDDING
|
||||
if any(sub_name in layer_name for sub_name in layer_norms):
|
||||
return LayerType.LAYER_NORM
|
||||
else:
|
||||
return LayerType.NONE
|
||||
|
||||
|
||||
class FalconMapper(converter_base.LayerActionMapperBase):
|
||||
"""LayerActionMapper for handling the Falcon-rw-1b model."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
is_symmetric: bool,
|
||||
attention_quant_bits: int,
|
||||
feedforward_quant_bits: int,
|
||||
embedding_quant_bits: int,
|
||||
backend: str,
|
||||
reader: _PytorchReader,
|
||||
):
|
||||
super().__init__(
|
||||
is_symmetric=is_symmetric,
|
||||
attention_quant_bits=attention_quant_bits,
|
||||
feedforward_quant_bits=feedforward_quant_bits,
|
||||
embedding_quant_bits=embedding_quant_bits,
|
||||
backend=backend,
|
||||
)
|
||||
self._reader = reader
|
||||
|
||||
def map_to_actions(
|
||||
self, layer_name: str
|
||||
) -> Optional[List[converter_base.QuantizationAction]]:
|
||||
"""Map the given layer name to actions."""
|
||||
actions = []
|
||||
tensor_value = self._reader.read_tensor_as_numpy(layer_name)
|
||||
if "query_key_value" in layer_name:
|
||||
qkv_tensors = self._decompose_falcon_qkv(tensor_value)
|
||||
for tensor, name in zip(qkv_tensors, ["q", "k", "v"]):
|
||||
decomposed_name = layer_name.replace("query_key_value", name)
|
||||
action = self._map_to_action_helper(tensor, decomposed_name)
|
||||
actions.append(action)
|
||||
else:
|
||||
actions.append(self._map_to_action_helper(tensor_value, layer_name))
|
||||
return actions
|
||||
|
||||
def _map_to_action_helper(
|
||||
self, tensor_value: np.ndarray, layer_name: str
|
||||
) -> converter_base.QuantizationAction:
|
||||
quantize_axis = None
|
||||
quantize_bits = None
|
||||
layer_type = LayerType.get_layer_type(layer_name)
|
||||
|
||||
if layer_type != LayerType.LAYER_NORM and layer_name.endswith(".weight"):
|
||||
layer_type = LayerType.get_layer_type(layer_name)
|
||||
quantize_axis = [0]
|
||||
if layer_type == LayerType.FEEDFORWARD:
|
||||
quantize_bits = self._feedforward_quant_bits
|
||||
elif layer_type == LayerType.ATTENTION:
|
||||
quantize_bits = self._attention_quant_bits
|
||||
if self._backend == "cpu" and ".dense." in layer_name:
|
||||
tensor_value = np.transpose(tensor_value)
|
||||
quantize_axis = [1]
|
||||
elif layer_type == LayerType.EMBEDDING:
|
||||
quantize_bits = self._embedding_quant_bits
|
||||
if self._backend == "cpu" and "word_embeddings" in layer_name:
|
||||
tensor_value = np.transpose(tensor_value)
|
||||
quantize_axis = [1]
|
||||
target_name = self.update_target_name(layer_name)
|
||||
|
||||
return converter_base.QuantizationAction(
|
||||
tensor_name=layer_name,
|
||||
tensor_value=tensor_value,
|
||||
target_name=target_name,
|
||||
quantize_axis=quantize_axis,
|
||||
quantize_bits=quantize_bits,
|
||||
pack_dim=0,
|
||||
)
|
||||
|
||||
def _decompose_falcon_qkv(self, tensor_value: np.ndarray) -> List[np.ndarray]:
|
||||
"""Decomposes combined qkv tensor used in falcon model into separate q, k and v tensors."""
|
||||
chunk_size = 64
|
||||
hidden_size = 2048
|
||||
|
||||
tensor_value = tensor_value.transpose()
|
||||
|
||||
q_tensor = np.zeros(
|
||||
(hidden_size,)
|
||||
+ ((hidden_size,) if len(tensor_value.shape) == 2 else ()),
|
||||
dtype=tensor_value.dtype,
|
||||
)
|
||||
k_tensor = np.zeros_like(q_tensor, dtype=tensor_value.dtype)
|
||||
v_tensor = np.zeros_like(k_tensor, dtype=tensor_value.dtype)
|
||||
|
||||
j = 0
|
||||
for i in range(0 * chunk_size, hidden_size * 3, chunk_size * 3):
|
||||
q_tensor[j : j + chunk_size] = tensor_value[i : i + chunk_size]
|
||||
j += chunk_size
|
||||
|
||||
j = 0
|
||||
for i in range(1 * chunk_size, hidden_size * 3, chunk_size * 3):
|
||||
k_tensor[j : j + chunk_size] = tensor_value[i : i + chunk_size]
|
||||
j += chunk_size
|
||||
|
||||
j = 0
|
||||
for i in range(2 * chunk_size, hidden_size * 3, chunk_size * 3):
|
||||
v_tensor[j : j + chunk_size] = tensor_value[i : i + chunk_size]
|
||||
j += chunk_size
|
||||
|
||||
return [
|
||||
np.ascontiguousarray(q_tensor.transpose()),
|
||||
np.ascontiguousarray(k_tensor.transpose()),
|
||||
np.ascontiguousarray(v_tensor.transpose()),
|
||||
]
|
||||
|
||||
def update_target_name(self, target_name: str) -> str:
|
||||
"""Updates the target name to match the tensor name convention."""
|
||||
layer_type = LayerType.get_layer_type(target_name)
|
||||
|
||||
target_name = target_name.replace(
|
||||
"transformer.h.", "params.lm.transformer.x_layers_"
|
||||
)
|
||||
|
||||
if layer_type == LayerType.FEEDFORWARD:
|
||||
target_name = target_name.replace(".weight", ".linear.w")
|
||||
target_name = target_name.replace(".bias", ".bias.b")
|
||||
target_name = target_name.replace(
|
||||
"mlp.dense_h_to_4h", "ff_layer.ffn_layer1"
|
||||
)
|
||||
target_name = target_name.replace(
|
||||
"mlp.dense_4h_to_h", "ff_layer.ffn_layer2"
|
||||
)
|
||||
elif layer_type == LayerType.ATTENTION:
|
||||
target_name = target_name.replace("dense", "post")
|
||||
target_name = target_name.replace(".weight", ".linear.w")
|
||||
target_name = target_name.replace(".bias", ".bias.b")
|
||||
elif layer_type == LayerType.EMBEDDING:
|
||||
target_name = target_name.replace(
|
||||
"transformer.word_embeddings", "params.lm.token_embedding"
|
||||
)
|
||||
target_name = target_name.replace(
|
||||
"lm_head", "params.lm.softmax.logits_ffn"
|
||||
)
|
||||
target_name = target_name.replace(".weight", ".w")
|
||||
elif layer_type == LayerType.LAYER_NORM:
|
||||
target_name = target_name.replace("input_layernorm", "pre_layer_norm")
|
||||
target_name = target_name.replace(
|
||||
"pre_layer_norm.weight", "pre_layer_norm.scale"
|
||||
)
|
||||
if self._backend == "cpu":
|
||||
target_name = target_name.replace(
|
||||
"post_attention_layernorm", "ff_layer.pre_layer_norm"
|
||||
)
|
||||
target_name = target_name.replace(
|
||||
"ff_layer.pre_layer_norm.weight", "ff_layer.pre_layer_norm.scale"
|
||||
)
|
||||
else:
|
||||
target_name = target_name.replace(
|
||||
"post_attention_layernorm", "post_layer_norm"
|
||||
)
|
||||
target_name = target_name.replace(
|
||||
"post_layer_norm.weight", "post_layer_norm.scale"
|
||||
)
|
||||
target_name = target_name.replace(
|
||||
"transformer.ln_f.weight", "params.lm.final_ln.scale"
|
||||
)
|
||||
target_name = target_name.replace(
|
||||
"transformer.ln_f.bias", "params.lm.final_ln.bias"
|
||||
)
|
||||
|
||||
return target_name
|
||||
|
||||
|
||||
class PytorchCkptLoader(converter_base.CkptLoaderBase):
|
||||
"""CkptLoader implementation for loading the Pytorch model."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ckpt_path: str,
|
||||
is_symmetric: bool,
|
||||
attention_quant_bits: int,
|
||||
feedforward_quant_bits: int,
|
||||
embedding_quant_bits: int,
|
||||
special_model: str,
|
||||
backend: str,
|
||||
):
|
||||
"""Initializes the loader.
|
||||
|
||||
Args:
|
||||
ckpt_path: The filepath to the safetensors file.
|
||||
is_symmetric: Whether to apply symmetric or asymmetric quantization.
|
||||
attention_quant_bits: An integer that specify the target quantization bits
|
||||
(support 8 or 4) for the attention layers.
|
||||
feedforward_quant_bits: An integer that specify the target quantization
|
||||
bits (support 8 or 4) for the feedforward layers in each Transformer
|
||||
blocks.
|
||||
embedding_quant_bits: An integer that specify the target quantization bits
|
||||
(support 8 or 4) for the embedding (and the final projection) layers.
|
||||
special_model: A string that indicates which input model is and whether
|
||||
any special treatment is needed.
|
||||
backend: A string indicating the backend used when converting this model.
|
||||
Valid options are "cpu" and "gpu".
|
||||
"""
|
||||
super().__init__(
|
||||
ckpt_path,
|
||||
is_symmetric,
|
||||
attention_quant_bits,
|
||||
feedforward_quant_bits,
|
||||
embedding_quant_bits,
|
||||
)
|
||||
|
||||
self._special_model = special_model
|
||||
self._reader = _PytorchReader(ckpt_path)
|
||||
if special_model in ["FALCON_RW_1B"]:
|
||||
self.mapper = FalconMapper(
|
||||
is_symmetric,
|
||||
attention_quant_bits,
|
||||
feedforward_quant_bits,
|
||||
embedding_quant_bits,
|
||||
backend,
|
||||
self._reader,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unknown special model: {special_model}")
|
||||
|
||||
def load_to_actions(
|
||||
self,
|
||||
) -> Iterator[List[converter_base.QuantizationAction]]:
|
||||
tensor_names = self._reader.get_tensor_names()
|
||||
for tensor_name in tensor_names:
|
||||
tensor_actions = self.mapper.map_to_actions(tensor_name)
|
||||
if tensor_actions is None:
|
||||
continue
|
||||
yield tensor_actions
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
# Copyright 2024 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Unit tests for pytorch_converter."""
|
||||
|
||||
import os
|
||||
|
||||
from absl.testing import absltest
|
||||
from absl.testing import parameterized
|
||||
|
||||
from mediapipe.tasks.python.genai.converter import pytorch_converter
|
||||
from mediapipe.tasks.python.test import test_utils
|
||||
|
||||
_TEST_DATA_DIR = 'mediapipe/tasks/testdata/text'
|
||||
_PYTORCH_FILE = test_utils.get_test_data_path(
|
||||
os.path.join(_TEST_DATA_DIR, 'falcon_rw_1b_test_weight.pt')
|
||||
)
|
||||
|
||||
|
||||
class PytorchConverterTest(parameterized.TestCase):
|
||||
VARIABLE_NAMES = [
|
||||
'transformer.word_embeddings.weight',
|
||||
'transformer.h.0.input_layernorm.weight',
|
||||
'transformer.h.0.input_layernorm.bias',
|
||||
'transformer.h.0.self_attention.query_key_value.weight',
|
||||
'transformer.h.0.self_attention.query_key_value.bias',
|
||||
'transformer.h.0.self_attention.dense.weight',
|
||||
'transformer.h.0.self_attention.dense.bias',
|
||||
'transformer.h.0.post_attention_layernorm.weight',
|
||||
'transformer.h.0.post_attention_layernorm.bias',
|
||||
'transformer.h.0.mlp.dense_h_to_4h.weight',
|
||||
'transformer.h.0.mlp.dense_h_to_4h.bias',
|
||||
'transformer.h.0.mlp.dense_4h_to_h.weight',
|
||||
'transformer.h.0.mlp.dense_4h_to_h.bias',
|
||||
'transformer.ln_f.weight',
|
||||
'transformer.ln_f.bias',
|
||||
'lm_head.weight',
|
||||
]
|
||||
|
||||
def test_init(self):
|
||||
loader = pytorch_converter.PytorchCkptLoader(
|
||||
ckpt_path=_PYTORCH_FILE,
|
||||
is_symmetric=True,
|
||||
attention_quant_bits=8,
|
||||
feedforward_quant_bits=8,
|
||||
embedding_quant_bits=8,
|
||||
special_model='FALCON_RW_1B',
|
||||
backend='cpu',
|
||||
)
|
||||
self.assertEqual(loader._ckpt_path, _PYTORCH_FILE)
|
||||
self.assertEqual(loader._is_symmetric, True)
|
||||
self.assertEqual(loader._attention_quant_bits, 8)
|
||||
self.assertEqual(loader._feedforward_quant_bits, 8)
|
||||
|
||||
@parameterized.product(
|
||||
quant_bits=(4, 8),
|
||||
)
|
||||
def test_load_to_actions(self, quant_bits):
|
||||
loader = pytorch_converter.PytorchCkptLoader(
|
||||
ckpt_path=_PYTORCH_FILE,
|
||||
is_symmetric=True,
|
||||
attention_quant_bits=8,
|
||||
feedforward_quant_bits=quant_bits,
|
||||
embedding_quant_bits=8,
|
||||
special_model='FALCON_RW_1B',
|
||||
backend='cpu',
|
||||
)
|
||||
actions = loader.load_to_actions()
|
||||
# There are 16 layers in the model, but qkv weight and bias would be
|
||||
# decomposed to q, k, v tensors, so there would be 20 quantization actions.
|
||||
self.assertEqual(sum(len(action) for action in actions), 20)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
absltest.main()
|
||||
+694
@@ -0,0 +1,694 @@
|
||||
# Copyright 2024 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Utilities for quantizing tensors.
|
||||
|
||||
Note that this is a reduced fork version of the praxis libraries to provide a
|
||||
self-contained library for packaging.
|
||||
"""
|
||||
|
||||
from typing import Any, List, Optional, Sequence, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from mediapipe.tasks.python.genai.converter import external_dependencies
|
||||
|
||||
jax = external_dependencies.jax
|
||||
jnp = external_dependencies.jnp
|
||||
lax = external_dependencies.lax
|
||||
|
||||
JTensor = jax.Array
|
||||
_UINT4_ZP = 8 # Default zero point for unsigned 4-bit.
|
||||
|
||||
# The following values are taken from fake_quantizer.py in Gemax.
|
||||
_MSE_QUANT_MULS = {
|
||||
8: 0.05408,
|
||||
4: 0.37755,
|
||||
}
|
||||
|
||||
|
||||
def _get_scan_range() -> np.ndarray:
|
||||
# Produce candidate scan values.
|
||||
return np.linspace(1.0, 0.5, num=11)
|
||||
|
||||
|
||||
def _get_mean_error(bound, t, min_value, max_value, p_value):
|
||||
scale = bound / max_value
|
||||
candidate = jnp.divide(t, scale)
|
||||
candidate = jnp.clip(jnp.round(candidate), min_value, max_value)
|
||||
candidate = jnp.multiply(candidate, scale)
|
||||
pmean_error = jnp.mean(jnp.abs(jnp.subtract(candidate, t)) ** p_value)
|
||||
return pmean_error
|
||||
|
||||
|
||||
def _get_best_bound_per_tensor(
|
||||
t: JTensor,
|
||||
bound: JTensor,
|
||||
min_value: float,
|
||||
max_value: float,
|
||||
p_value: float = 1.0,
|
||||
) -> JTensor:
|
||||
"""Scan around [0.5, 1] * hard max value to get bound value for whole tensor.
|
||||
|
||||
This does a scan to get bound value(s) that minimize mean absolute error (MAE)
|
||||
between original tensor 't' and quantized tensor. It's (almost) equivalent to
|
||||
maximizing entropy.
|
||||
|
||||
Args:
|
||||
t: The input float tensor.
|
||||
bound: The hard max value for tensor 't'. It has the same length as shape.
|
||||
min_value: Minimal value for the quantization bound.
|
||||
max_value: Maximal value for the quantization bound.
|
||||
p_value: Exponent of the p-mean error metric. Default to 1.0 which is MAE.
|
||||
|
||||
Returns:
|
||||
The best bound values for 't', that minimize p-mean error.
|
||||
"""
|
||||
|
||||
def _quant(scaling_factors):
|
||||
return _get_mean_error(
|
||||
bound * scaling_factors, t, min_value, max_value, p_value
|
||||
)
|
||||
|
||||
scaling_factors = _get_scan_range()
|
||||
diffs = jax.vmap(_quant)(scaling_factors)
|
||||
best_scaling = scaling_factors[jnp.argmin(diffs)].astype(bound.dtype)
|
||||
return bound * best_scaling
|
||||
|
||||
|
||||
def _quantrow(
|
||||
vec: JTensor,
|
||||
bound: JTensor,
|
||||
min_value: float,
|
||||
max_value: float,
|
||||
p_value: float,
|
||||
factors: np.ndarray,
|
||||
) -> JTensor:
|
||||
"""Get best rescaling factor from a list of factors applied a channel.
|
||||
|
||||
Args:
|
||||
vec: The vector in a channel.
|
||||
bound: The hard bound (max(abs(vec))) of the vector.
|
||||
min_value: The target min value.
|
||||
max_value: The target max value.
|
||||
p_value: Exponent of the p-mean error metric.
|
||||
factors: The values to be applied on top of bound.
|
||||
|
||||
Returns:
|
||||
adjusted bound value out of the list of factors applied to bound.
|
||||
"""
|
||||
|
||||
def _quant(bounds):
|
||||
return _get_mean_error(bounds, vec, min_value, max_value, p_value)
|
||||
|
||||
diffs = jax.vmap(_quant)(bound * factors)
|
||||
best_scaling = factors[jnp.argmin(diffs)]
|
||||
return bound * best_scaling
|
||||
|
||||
|
||||
def _get_best_bound_per_channel(
|
||||
t: JTensor,
|
||||
bound: JTensor,
|
||||
min_value: float,
|
||||
max_value: float,
|
||||
p_value: float = 1.0,
|
||||
) -> JTensor:
|
||||
"""Scan around [0.5, 1] * hard max value to get bound value for each channel.
|
||||
|
||||
This does a scan to get bound value(s) that minimize mean absolute error (MAE)
|
||||
between original tensor 't' and quantized tensor. It's (almost) equivalent to
|
||||
maximizing entropy.
|
||||
|
||||
Args:
|
||||
t: The input float tensor.
|
||||
bound: The hard max value for tensor 't'. It has the same length as shape.
|
||||
min_value: Minimal value for the quantization bound.
|
||||
max_value: Maximal value for the quantization bound.
|
||||
p_value: Exponent of the p-mean error metric. Default to 1.0 which is MAE.
|
||||
|
||||
Returns:
|
||||
The best bound values for 't', that minimize p-mean error.
|
||||
"""
|
||||
assert len(t.shape) == 2
|
||||
assert len(bound.shape) == 2
|
||||
assert t.shape[1] == bound.shape[1]
|
||||
assert bound.shape[0] == 1
|
||||
scans = _get_scan_range()
|
||||
|
||||
def _quant(tensor, bound, min_value, max_value, p_value, factors):
|
||||
ret = np.zeros(bound.shape)
|
||||
for i in range(len(tensor)):
|
||||
best = _quantrow(
|
||||
tensor[i], bound[i], min_value, max_value, p_value, factors
|
||||
)
|
||||
ret[i] = best
|
||||
return ret
|
||||
|
||||
t = t.transpose()
|
||||
t_split = list(t)
|
||||
res = _quant(t_split, bound[0, :], min_value, max_value, p_value, scans)
|
||||
res = res.reshape(bound.shape)
|
||||
return res
|
||||
|
||||
|
||||
def get_best_bound(
|
||||
t: JTensor,
|
||||
bound: JTensor,
|
||||
min_value: float,
|
||||
max_value: float,
|
||||
p_value: float = 1.0,
|
||||
per_channel: bool = False,
|
||||
) -> JTensor:
|
||||
"""Scan multiple factors on max value to get best bound value.
|
||||
|
||||
This does a scan to get bound value(s) that minimize mean absolute error (MAE)
|
||||
between original tensor 't' and quantized tensor. It's (almost) equivalent to
|
||||
maximizing entropy.
|
||||
|
||||
Args:
|
||||
t: The input float tensor.
|
||||
bound: The hard max value for tensor 't'. It has the same length as shape.
|
||||
min_value: Minimal value for the quantization bound.
|
||||
max_value: Maximal value for the quantization bound.
|
||||
p_value: Exponent of the p-mean error metric. Default to 1.0 which is MAE.
|
||||
per_channel: if get best bound for entire tensor or per channel.
|
||||
|
||||
Returns:
|
||||
The best bound values for 't', that minimize p-mean error.
|
||||
"""
|
||||
if per_channel:
|
||||
return _get_best_bound_per_channel(t, bound, min_value, max_value, p_value)
|
||||
else:
|
||||
return _get_best_bound_per_tensor(t, bound, min_value, max_value, p_value)
|
||||
|
||||
|
||||
def get_min_max(
|
||||
bits: int = 8,
|
||||
unsigned: bool = False,
|
||||
use_fp: bool = False,
|
||||
) -> Tuple[float, float]:
|
||||
"""Gets the min/max range for a given number of bits.
|
||||
|
||||
Args:
|
||||
bits: Target number of bits for quantization.
|
||||
unsigned: If True compute min and max for unsigned number, else for signed.
|
||||
use_fp: in floating point.
|
||||
|
||||
Returns:
|
||||
min/max values for the provide number of bits.
|
||||
"""
|
||||
if use_fp:
|
||||
# TODO: support other fp types.
|
||||
return -448.0, 448.0
|
||||
# Calculation instead of jax.iinfo is used to support bits beside 4 and 8.
|
||||
if unsigned:
|
||||
# For unsigned 8 bits precision it is [0, 255]
|
||||
return 0, 2**bits - 1
|
||||
else:
|
||||
# For signed 8 bits precision it is [-128, 127]
|
||||
return -1 * 2 ** (bits - 1), 2 ** (bits - 1) - 1
|
||||
|
||||
|
||||
def pass_through(x: JTensor, fn: Any) -> JTensor:
|
||||
# Create an exactly-zero expression with Sterbenz lemma that has an
|
||||
# exactly-one gradient.
|
||||
return x - jax.lax.stop_gradient(x) + jax.lax.stop_gradient(fn(x))
|
||||
|
||||
|
||||
def mse_reduce_precision(
|
||||
t: JTensor,
|
||||
contract_dims: Optional[Sequence[int]],
|
||||
need_gradient: bool = False,
|
||||
bits: int = 8,
|
||||
use_symmetric: bool = True,
|
||||
use_fp: bool = False,
|
||||
) -> Tuple[JTensor, JTensor, Optional[JTensor]]:
|
||||
"""Reduce the precision of a tensor using MSE quantization.
|
||||
|
||||
Args:
|
||||
t: Input tensor.
|
||||
contract_dims: Specifies contracting dimensions of the input tensor.
|
||||
need_gradient: If gradient is needed out of this function.
|
||||
bits: Target number of bits.
|
||||
use_symmetric: If the input tensor is quantized symmetrically.
|
||||
use_fp: Use floating point.
|
||||
|
||||
Returns:
|
||||
A tuple of quantized tensor, quantization scale
|
||||
and quantization zero point (optional).
|
||||
"""
|
||||
if need_gradient:
|
||||
raise ValueError('need_gradient is not supported for mse_reduce_precision.')
|
||||
if use_fp:
|
||||
raise ValueError('use_fp is not supported for mse_reduce_precision.')
|
||||
if not use_symmetric:
|
||||
raise ValueError('Gemax mse_quant is typically symmetric.')
|
||||
|
||||
scale_multiplier = _MSE_QUANT_MULS.get(bits)
|
||||
if scale_multiplier is None:
|
||||
raise ValueError(f'MSE_QUANT_MULS not defined for {bits} bits.')
|
||||
rms = jnp.sqrt(jnp.mean(t**2, axis=contract_dims, keepdims=True))
|
||||
scale = scale_multiplier * rms + 1e-12 # Add epsilon as in Gemax
|
||||
|
||||
# Calculate the zero point used in fake_quantizer.mse_quant with narrow range.
|
||||
maxq = 2**bits - 1
|
||||
zero_mse_quant = (maxq - 1) / 2.0 # This is 7 for 4 bits
|
||||
|
||||
# Simulate the quantization steps from fake_quantizer.mse_quant:
|
||||
# 1. Scale and shift: t / scale + zero_mse_quant
|
||||
t_shifted = jnp.divide(t, scale) + zero_mse_quant
|
||||
# 2. Rounding
|
||||
q_rounded = jnp.round(t_shifted)
|
||||
# 3. Clipping to [0, maxq - 1]
|
||||
q_clipped = jnp.clip(q_rounded, 0, maxq - 1) # [0, 14] for 4 bits
|
||||
|
||||
# The quantized value (qvar) to be stored should be
|
||||
# `q_clipped - zero_mse_quant`. This makes the dequantization match:
|
||||
# (qvar - zp) * scale == (q_clipped - zero_mse_quant) * scale
|
||||
qvar = q_clipped - zero_mse_quant # Ranges from [-7, 7] for 4 bits
|
||||
|
||||
# ml_drift expects a zero point for dequantization. To match the effective
|
||||
# dequantization of mse_quant, we set the zero point to 0 (or None).
|
||||
zp = None
|
||||
|
||||
container_dtype = (
|
||||
jnp.int8 if bits <= 8 else jnp.int16 if bits <= 16 else jnp.int32
|
||||
)
|
||||
qvar = qvar.astype(container_dtype)
|
||||
|
||||
return qvar, scale, zp
|
||||
|
||||
|
||||
def reduce_precision(
|
||||
t: JTensor,
|
||||
contract_dims: Optional[Sequence[int]],
|
||||
need_gradient: bool = False,
|
||||
bits: int = 8,
|
||||
optimization_on_bound: bool = False,
|
||||
p_value: float = 1.0,
|
||||
percentile: float = 1.0,
|
||||
use_symmetric: bool = True,
|
||||
use_fp: bool = False,
|
||||
add_scale_eps: bool = False,
|
||||
per_channel: bool = False,
|
||||
random_rounding: bool = False,
|
||||
key: Optional[jax.Array] = None,
|
||||
) -> Tuple[JTensor, JTensor, Optional[JTensor]]:
|
||||
"""Reduce the precision of a tensor.
|
||||
|
||||
Generic for all tensors.
|
||||
|
||||
Args:
|
||||
t: Input tensor.
|
||||
contract_dims: Specifies contracting dimensions of the input tensor.
|
||||
need_gradient: If gradient is needed out of this function.
|
||||
bits: Target number of bits.
|
||||
optimization_on_bound: If MAE bound optimizer is used.
|
||||
p_value: Exponent of the p-mean error metric. Default to 1.0 which is MAE.
|
||||
percentile: Percentile Factor to apply on the min/max range. Setting this to
|
||||
other than 1.0 disables optimization_on_bound.
|
||||
use_symmetric: If the input tensor is quantized symmetrically.
|
||||
use_fp: Use floating point.
|
||||
add_scale_eps: Add eps value or replace zero value by 1 to avoid division by
|
||||
zero.
|
||||
per_channel: use per-channel clipping optimization.
|
||||
random_rounding: round with uniform random.
|
||||
key: rng key for rounding.
|
||||
|
||||
Returns:
|
||||
A tuple of quantized tensor, quantization scale
|
||||
and quantization zero point (optional).
|
||||
"""
|
||||
min_value, max_value = get_min_max(bits, use_fp=use_fp)
|
||||
|
||||
if use_symmetric:
|
||||
bound = jnp.max(jnp.abs(t), axis=contract_dims, keepdims=True)
|
||||
scale_bound = max_value
|
||||
else:
|
||||
t_max = jnp.max(t, axis=contract_dims, keepdims=True)
|
||||
t_min = jnp.min(t, axis=contract_dims, keepdims=True)
|
||||
bound = t_max - t_min
|
||||
scale_bound = max_value - min_value
|
||||
|
||||
if percentile < 1.0:
|
||||
bound = jnp.multiply(bound, percentile)
|
||||
elif optimization_on_bound:
|
||||
bound = get_best_bound(
|
||||
t, bound, min_value, max_value, p_value, per_channel=per_channel
|
||||
)
|
||||
|
||||
scale = bound / scale_bound
|
||||
|
||||
if add_scale_eps:
|
||||
# Add epsilon to avoid divide-by-zero.
|
||||
scale = scale + jnp.finfo(t.dtype).eps
|
||||
else:
|
||||
scale = jnp.where(scale == 0.0, 1.0, scale)
|
||||
|
||||
if use_symmetric:
|
||||
zp = None
|
||||
t = jnp.divide(t, scale)
|
||||
else:
|
||||
zp = min_value - t_min / scale
|
||||
t = jnp.divide(t, scale) + zp
|
||||
zp = jnp.multiply(scale, zp)
|
||||
|
||||
if use_fp:
|
||||
# No need to round.
|
||||
t = jnp.clip(t, min_value, max_value).astype(jnp.float8_e4m3fn)
|
||||
# TODO: refactor to remove this logic.
|
||||
t = jax.lax.bitcast_convert_type(t, new_dtype=jnp.int8)
|
||||
else:
|
||||
if need_gradient:
|
||||
t = pass_through(t, jnp.round)
|
||||
t = jnp.clip(t, min_value, max_value)
|
||||
else:
|
||||
if random_rounding:
|
||||
t = t + jax.random.uniform(
|
||||
key=key, shape=t.shape, minval=-0.5, maxval=0.5
|
||||
)
|
||||
t = jnp.round(t)
|
||||
container_dtype = (
|
||||
jnp.int8 if bits <= 8 else jnp.int16 if bits <= 16 else jnp.int32
|
||||
)
|
||||
t = jnp.clip(t, min_value, max_value).astype(container_dtype)
|
||||
|
||||
return t, scale, zp
|
||||
|
||||
|
||||
def quantize_tensor(
|
||||
var: np.ndarray,
|
||||
axis: List[int],
|
||||
factor: float = 1.0,
|
||||
sym: bool = True,
|
||||
number_bits: int = 8,
|
||||
use_fp: bool = False,
|
||||
add_scale_eps: bool = False,
|
||||
optimization_on_bound: bool = False,
|
||||
p_value: float = 1.0,
|
||||
per_channel: bool = False,
|
||||
block_size: int = 0,
|
||||
use_mse_quant: bool = False,
|
||||
) -> Union[
|
||||
Tuple[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray, np.ndarray]
|
||||
]:
|
||||
"""Quantize a tensor.
|
||||
|
||||
Args:
|
||||
var: The variable to be quantized.
|
||||
axis: The axis along which variable will be quantized.
|
||||
factor: The clipping factor.
|
||||
sym: Symmetric or asymmetric quantize the variable.
|
||||
number_bits: Number of bits for quantized value.
|
||||
use_fp: do fp with number of bits (i.e. fp8)
|
||||
add_scale_eps: add epsilon to scale to avoid division by zero, else it will
|
||||
replace zero scale by 1.
|
||||
optimization_on_bound: If p-mean bound optimizer is used.
|
||||
p_value: Exponent of the p-mean error metric. Default to 1.0 which is MAE.
|
||||
per_channel: use per-channel clipping optimization.
|
||||
block_size: block size for sub-channel quantization. Defaults to 0, which
|
||||
means off.
|
||||
use_mse_quant: Whether to use MSE quantization.
|
||||
|
||||
Returns:
|
||||
Quantized tensors, along with scales and zero point.
|
||||
"""
|
||||
# TODO: support jnp.float8_e5m2
|
||||
assert number_bits == 8 or number_bits == 4
|
||||
jnp_var = jnp.asarray(var)
|
||||
# When using sub-channel, the contracting dim is split into a sub-channel
|
||||
# dim followed by the block dim. Therefore the contracting dim
|
||||
# (quantize_axis) should increment by one, and the corresponding pack_dim
|
||||
# should also increment by one.
|
||||
if block_size > 0:
|
||||
# We hard-code in a special case for [0, 1]. It has not been thoroughly
|
||||
# tested or verified yet, so we log a warning.
|
||||
if len(axis) == 2 and axis == [0, 1]:
|
||||
print('Using experimental blockwise int4 axis=[0, 1] compression')
|
||||
jnp_var = jnp.reshape(jnp_var, [-1, jnp_var.shape[-1]])
|
||||
axis = [0]
|
||||
shape = list(jnp_var.shape)
|
||||
assert len(axis) == 1, 'Only support 1D sub-channel quantization'
|
||||
sub_channels, rem = divmod(shape[axis[0]], block_size)
|
||||
assert rem == 0
|
||||
shape.insert(axis[0], sub_channels)
|
||||
axis[0] += 1
|
||||
shape[axis[0]] = block_size
|
||||
jnp_var = jnp.reshape(jnp_var, shape)
|
||||
|
||||
if use_mse_quant:
|
||||
qvar, scale, zp = mse_reduce_precision(
|
||||
jnp_var,
|
||||
contract_dims=axis,
|
||||
need_gradient=False,
|
||||
bits=number_bits,
|
||||
use_symmetric=sym,
|
||||
use_fp=False,
|
||||
)
|
||||
else:
|
||||
qvar, scale, zp = reduce_precision(
|
||||
jnp_var,
|
||||
contract_dims=axis,
|
||||
need_gradient=False,
|
||||
bits=number_bits,
|
||||
optimization_on_bound=optimization_on_bound,
|
||||
percentile=factor,
|
||||
use_symmetric=sym,
|
||||
use_fp=use_fp,
|
||||
add_scale_eps=add_scale_eps,
|
||||
p_value=p_value,
|
||||
per_channel=per_channel,
|
||||
)
|
||||
if sym:
|
||||
return np.array(qvar), np.array(jnp.squeeze(scale, axis=axis)) # pytype: disable=wrong-arg-types # jnp-type
|
||||
else:
|
||||
return (
|
||||
np.array(qvar),
|
||||
# CAVEAT: the following squeezes should squeeze along the quantization
|
||||
# axis only.
|
||||
np.array(jnp.squeeze(scale)),
|
||||
np.array(jnp.squeeze(zp)),
|
||||
)
|
||||
|
||||
|
||||
def pack_4bit(
|
||||
x: np.ndarray, pack_dim: int, packed_dtype: jnp.dtype = jnp.int32
|
||||
) -> np.ndarray:
|
||||
"""Pack int8 or uint8 tensor where its values are actually int4 or uint4, to int32 or int8 nibble format along pack_dim.
|
||||
|
||||
Args:
|
||||
x: Original int8 or uint8 tensor to pack.
|
||||
pack_dim: Dimension to pack along. x.shape[pack_dim] must be divisible by 8,
|
||||
when packed_dtype is int32 and divisible by 2 when target_type is int8.
|
||||
Also pack_dim must be < x.ndim - 1.
|
||||
packed_dtype: Target type to pack to, int32 or int8.
|
||||
|
||||
Returns:
|
||||
int32 or int8 packed tensor where the pack_dim size is dividend by 8
|
||||
from the original tensor x.
|
||||
"""
|
||||
x = jnp.asarray(x)
|
||||
if packed_dtype == jnp.int8 and x.dtype == jnp.uint8:
|
||||
# It doesn't make sense to pack uint8 numbers into int4 as we'll
|
||||
# the range overlap between uint8 and int4 is [0..7].
|
||||
raise ValueError(
|
||||
'only int8 input dtype is supported when packing into int8. '
|
||||
f'Given {x.dtype}'
|
||||
)
|
||||
|
||||
if x.dtype != jnp.int8 and x.dtype != jnp.uint8:
|
||||
raise ValueError(
|
||||
f'input dtype must be either int8 or uint8. Given {x.dtype}'
|
||||
)
|
||||
if pack_dim >= x.ndim - 1:
|
||||
raise ValueError(
|
||||
f'pack_dim must be < input ndim - 1. input shape {x.shape} and pack_dim'
|
||||
f' {pack_dim}'
|
||||
)
|
||||
if packed_dtype != jnp.int32 and packed_dtype != jnp.int8:
|
||||
raise ValueError(
|
||||
f'packed_dtype must be either int32 or int8. Given {packed_dtype}'
|
||||
)
|
||||
if packed_dtype == jnp.int32 and x.shape[pack_dim] % 8 != 0:
|
||||
raise ValueError(
|
||||
'input shape[pack_dim] must be divisible by 8 when target_type '
|
||||
f'is int32. Given shape {x.shape}'
|
||||
)
|
||||
if packed_dtype == jnp.int8 and x.shape[pack_dim] % 2 != 0:
|
||||
raise ValueError(
|
||||
'input shape[pack_dim] must be divisible by 2 when target_type '
|
||||
f'is int8. Given shape {x.shape}'
|
||||
)
|
||||
|
||||
int4s_per_packed_type = 8 if packed_dtype == jnp.int32 else 2
|
||||
|
||||
rep_shape = list(x.shape)
|
||||
rep_shape.insert(pack_dim + 1, int4s_per_packed_type)
|
||||
rep_shape[pack_dim] //= int4s_per_packed_type
|
||||
|
||||
shifts = lax.broadcasted_iota(packed_dtype, rep_shape, pack_dim + 1)
|
||||
shifts <<= 2
|
||||
|
||||
# Promote x to packed_dtype
|
||||
x = x & jnp.array(0x0F, packed_dtype)
|
||||
x = lax.reshape(x, rep_shape)
|
||||
x = x << shifts
|
||||
x = lax.reduce(x, jnp.array(0x0, packed_dtype), lax.add, [pack_dim + 1])
|
||||
return np.asarray(x)
|
||||
|
||||
|
||||
def pack_2bit_into_int8(x: np.ndarray, pack_dim: int) -> np.ndarray:
|
||||
"""Pack (u)int8 tensor with int2 or uint2 values into int8 along pack_dim.
|
||||
|
||||
Args:
|
||||
x: Original int8 or uint8 tensor to pack.
|
||||
pack_dim: Dimension to pack along. x.shape[pack_dim] must be divisible by 4.
|
||||
Also pack_dim must be < x.ndim - 1.
|
||||
|
||||
Returns:
|
||||
int8 packed tensor where the pack_dim size is divided by 4 from the original
|
||||
tensor x.
|
||||
"""
|
||||
x = jnp.asarray(x)
|
||||
packed_dtype = jnp.int8
|
||||
bits = 2
|
||||
bits_per_packed_type = 8 // bits # 4 int2 values per int8
|
||||
|
||||
if x.dtype != jnp.int8 and x.dtype != jnp.uint8:
|
||||
raise ValueError(
|
||||
f'input dtype must be either int8 or uint8. Given {x.dtype}'
|
||||
)
|
||||
if pack_dim >= x.ndim - 1:
|
||||
raise ValueError(
|
||||
f'pack_dim must be < input ndim - 1. input shape {x.shape} and pack_dim'
|
||||
f' {pack_dim}'
|
||||
)
|
||||
if x.shape[pack_dim] % bits_per_packed_type != 0:
|
||||
raise ValueError(
|
||||
f'input shape[pack_dim] must be divisible by {bits_per_packed_type}'
|
||||
f' when packing {bits}-bit values into {packed_dtype}. Given shape'
|
||||
f' {x.shape}'
|
||||
)
|
||||
|
||||
rep_shape = list(x.shape)
|
||||
rep_shape.insert(pack_dim + 1, bits_per_packed_type)
|
||||
rep_shape[pack_dim] //= bits_per_packed_type
|
||||
|
||||
# Create shifts: [0, 1, 2, 3]
|
||||
shifts = lax.broadcasted_iota(packed_dtype, rep_shape, pack_dim + 1)
|
||||
# Adjust shifts for 2-bit packing: [0, 2, 4, 6]
|
||||
shifts <<= bits - 1 # Equivalent to shifts * 2
|
||||
|
||||
# Promote x to packed_dtype and mask to keep only the lowest 2 bits (0x03)
|
||||
x = x & jnp.array(0x03, packed_dtype)
|
||||
x = lax.reshape(x, rep_shape)
|
||||
x = x << shifts
|
||||
x = lax.reduce(x, jnp.array(0x0, packed_dtype), lax.add, [pack_dim + 1])
|
||||
return np.asarray(x)
|
||||
|
||||
|
||||
def update_to_uint4(
|
||||
qx: np.ndarray, scale: np.ndarray, zp: Optional[np.ndarray] = None
|
||||
):
|
||||
"""Updates the quantized weights from int4 to uint4.
|
||||
|
||||
This is a conversion function designed for XNNPack as it expects the 4-bit
|
||||
quantized weight to be represented differently from the original Pax setting.
|
||||
Specifically, the differences are:
|
||||
1) The dynamic range of weight values: int4 (Pax) vs. uint4 (XNNPack).
|
||||
2) The dynamic range of zero-point: float (Pax) vs. uint4 (XNNPack).
|
||||
3) The number of zero-point: per-channel (Pax) vs. per-tensor (XNNPack).
|
||||
|
||||
Args:
|
||||
qx: np.array of shape [..., channel], which is the quantized weight values
|
||||
from Pax in the shape of. The values are in the dynamic range of int4 but
|
||||
are hosted as int8 type. Note that if the first dimension is 3, it means
|
||||
the qkv matrices are concatenated together and should be treated
|
||||
differently.
|
||||
scale: np.array of shape [1(3), channel] as np.float type, which are the
|
||||
scaling factors for dequantization per channel.
|
||||
zp: (optional) np.array of shape [1 (or 3), channel] as np.float type, which
|
||||
are the zero points for dequantization per channel.
|
||||
|
||||
Returns:
|
||||
A tuple (qx, scale, zp):
|
||||
qx: The updated np.array of shape [..., channel] as np.int8 type with
|
||||
updated dynamic range as uint4 (with 8 as the default zero points).
|
||||
scale: Same as the input scale.
|
||||
zp: (optional) np.array of shape [1 (or 3)] as np.int8 type with the
|
||||
updated zero point values in the dynamic range as uint4.
|
||||
"""
|
||||
if qx.dtype != np.int8 or ('float' not in str(scale.dtype)):
|
||||
raise ValueError(
|
||||
'Unexpected dtype qx:' + str(qx.dtype) + ' scale:' + str(scale.dtype)
|
||||
)
|
||||
|
||||
scale = scale.astype(np.float32)
|
||||
|
||||
def get_new_zp(old_zp):
|
||||
new_zp = old_zp / (scale + np.finfo(np.float32).eps)
|
||||
per_tensor_zp = np.mean(new_zp)
|
||||
per_tensor_zp = per_tensor_zp.astype(np.int8) + _UINT4_ZP
|
||||
return per_tensor_zp
|
||||
|
||||
if zp is not None:
|
||||
if qx.shape[0] == 3:
|
||||
per_tensor_zp = np.stack([get_new_zp(szp) for szp in zp], axis=0)
|
||||
else:
|
||||
per_tensor_zp = get_new_zp(zp)
|
||||
else:
|
||||
per_tensor_zp = (
|
||||
_UINT4_ZP * np.ones(shape=(3)) if qx.shape[0] == 3 else _UINT4_ZP
|
||||
)
|
||||
|
||||
qx = qx + _UINT4_ZP
|
||||
return qx, scale, np.array(per_tensor_zp, dtype=np.int32)
|
||||
|
||||
|
||||
def max_length_axis(shape, axis):
|
||||
"""Get the axis with the maximum length."""
|
||||
if axis is None:
|
||||
axis = range(len(shape))
|
||||
elif isinstance(axis, int):
|
||||
axis = [axis]
|
||||
axis = tuple(a % len(shape) for a in axis)
|
||||
axis = sorted(axis, reverse=True) # Prefers larger dim if there is a tie.
|
||||
max_len_ind = max(enumerate([shape[a] for a in axis]), key=lambda x: x[1])[0]
|
||||
return axis[max_len_ind]
|
||||
|
||||
|
||||
def dequantize_blockwise(values, scales, block_size):
|
||||
"""Dequantizes a given blockwise-compressed tensor."""
|
||||
contracted_dims = []
|
||||
if len(values.shape) != len(scales.shape):
|
||||
raise ValueError('Values and scales are not the same size')
|
||||
|
||||
# Find the contracted dimensions.
|
||||
for i in range(len(values.shape)):
|
||||
if values.shape[i] != scales.shape[i]:
|
||||
contracted_dims.append(i)
|
||||
|
||||
# Make sure decompressing results in an exact shape match.
|
||||
test_shape = list(scales.shape)
|
||||
for i in contracted_dims:
|
||||
test_shape[i] *= block_size
|
||||
|
||||
if tuple(test_shape) != values.shape:
|
||||
raise ValueError('Expanded dimensions did not match')
|
||||
|
||||
# We need to repeat the scale tensor along the target dimension to match the
|
||||
# shape of the quantized value tensor.
|
||||
target_dim = max_length_axis(values.shape, contracted_dims)
|
||||
scales = jnp.repeat(scales, block_size, axis=target_dim)
|
||||
return values.astype(jnp.int32).astype(jnp.float32) * scales
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
# Copyright 2024 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for quantization_util."""
|
||||
|
||||
from absl.testing import absltest
|
||||
import jax
|
||||
from jax import numpy as jnp
|
||||
import numpy as np
|
||||
|
||||
from mediapipe.tasks.python.genai.converter import quantization_util
|
||||
|
||||
|
||||
_dtype = lambda x: getattr(x, 'dtype', None) or np.asarray(x).dtype
|
||||
|
||||
|
||||
class TestCase(absltest.TestCase):
|
||||
|
||||
def assertAllClose(
|
||||
self, x, y, check_dtypes=True, rtol=1e-5, atol=1e-5, **kwargs
|
||||
):
|
||||
"""Wrapper for np.testing.assert_allclose()."""
|
||||
x = np.asarray(x)
|
||||
y = np.asarray(y)
|
||||
if check_dtypes:
|
||||
self.assertDtypesMatch(x, y)
|
||||
x = x.astype(np.float32) if x.dtype == jnp.bfloat16 else x
|
||||
y = y.astype(np.float32) if y.dtype == jnp.bfloat16 else y
|
||||
np.testing.assert_allclose(x, y, rtol=rtol, atol=atol, **kwargs)
|
||||
|
||||
def assertDtypesMatch(self, x, y):
|
||||
self.assertEqual(
|
||||
jax.dtypes.canonicalize_dtype(_dtype(x)),
|
||||
jax.dtypes.canonicalize_dtype(_dtype(y)),
|
||||
)
|
||||
|
||||
|
||||
class Quantize8BTest(TestCase):
|
||||
|
||||
def test_quantize_symmetric(self):
|
||||
inputs = np.array([[1.2, 3.1, 5.5, 2.9], [0.2, -1.5, 3.3, 4.0]])
|
||||
qx, scale = quantization_util.quantize_tensor(inputs, axis=[1])
|
||||
|
||||
self.assertAllClose(
|
||||
qx, np.array([[28, 72, 127, 67], [6, -48, 105, 127]], dtype=np.int8)
|
||||
)
|
||||
self.assertAllClose(
|
||||
scale, np.array([0.04330709, 0.03149606], dtype=np.float32)
|
||||
)
|
||||
|
||||
def test_quantize_symmetric_with_dimension_size_one_unquantized(self):
|
||||
# inputs shape: (2, 1, 4), quantization axis 2.
|
||||
inputs = np.array([[[1.2, 3.1, 5.5, 2.9]], [[0.2, -1.5, 3.3, 4.0]]])
|
||||
qx, scale = quantization_util.quantize_tensor(inputs, axis=[2])
|
||||
|
||||
self.assertAllClose(
|
||||
qx, np.array([[[28, 72, 127, 67]], [[6, -48, 105, 127]]], dtype=np.int8)
|
||||
)
|
||||
# expected scale shape: (2, 1)
|
||||
self.assertAllClose(
|
||||
scale, np.array([[0.04330709], [0.03149606]], dtype=np.float32)
|
||||
)
|
||||
|
||||
def test_quantize_asymmetric(self):
|
||||
inputs = np.array([[1.2, 3.1, 5.5, 2.9], [0.2, -1.5, 3.3, 4.0]])
|
||||
qx, scale, zp = quantization_util.quantize_tensor(
|
||||
inputs, axis=[1], sym=False
|
||||
)
|
||||
|
||||
self.assertAllClose(
|
||||
qx,
|
||||
np.array([[-128, -15, 127, -27], [-49, -128, 95, 127]], dtype=np.int8),
|
||||
)
|
||||
self.assertAllClose(scale, np.array([0.016863, 0.021569], dtype=np.float32))
|
||||
self.assertAllClose(zp, np.array([-3.358431, -1.260784], dtype=np.float32))
|
||||
|
||||
|
||||
class Quantize8BFPTest(TestCase):
|
||||
|
||||
def test_quantize_symmetric(self):
|
||||
inputs = np.array([[1.0, 2.0, 5.5, 2.9], [0.02, -0.01, 3.3, 4.0]])
|
||||
qx, scale = quantization_util.quantize_tensor(inputs, axis=[1], use_fp=True)
|
||||
|
||||
self.assertAllClose(
|
||||
qx,
|
||||
np.array([[106, 114, 126, 119], [65, -71, 124, 126]], dtype=np.int8),
|
||||
)
|
||||
self.assertAllClose(
|
||||
scale, np.array([0.01227679, 0.00892857], dtype=np.float32)
|
||||
)
|
||||
|
||||
def test_quantize_symmetric_with_dimension_size_one_unquantized(self):
|
||||
# inputs shape: (2, 1, 4), quantization axis 2.
|
||||
inputs = np.array([[[1.0, 2.0, 5.5, 2.9]], [[0.02, -0.01, 3.3, 4.0]]])
|
||||
qx, scale = quantization_util.quantize_tensor(inputs, axis=[2], use_fp=True)
|
||||
|
||||
self.assertAllClose(
|
||||
qx,
|
||||
np.array(
|
||||
[[[106, 114, 126, 119]], [[65, -71, 124, 126]]], dtype=np.int8
|
||||
),
|
||||
)
|
||||
# expected scale shape: (2, 1)
|
||||
self.assertAllClose(
|
||||
scale, np.array([[0.01227679], [0.00892857]], dtype=np.float32)
|
||||
)
|
||||
|
||||
def test_quantize_asymmetric(self):
|
||||
inputs = np.array([[-1.0, -2.0, -2.01, 2.9], [0.02, -0.15, 3.3, 4.0]])
|
||||
qx, scale, zp = quantization_util.quantize_tensor(
|
||||
inputs, axis=[1], sym=False, use_fp=True
|
||||
)
|
||||
|
||||
self.assertAllClose(
|
||||
qx,
|
||||
np.array([[-8, -2, -2, 126], [-3, -2, 121, 126]], dtype=np.int8),
|
||||
)
|
||||
self.assertAllClose(
|
||||
scale, np.array([0.00547991, 0.0046317], dtype=np.float32)
|
||||
)
|
||||
self.assertAllClose(
|
||||
zp, np.array([-0.4449999, -1.9250002], dtype=np.float32)
|
||||
)
|
||||
|
||||
def test_quantize_add_scale_eps(self):
|
||||
inputs = np.array([[0.0, 0.0, 0.0, 0.0], [-4.0, -4.0, -4.0, -4.0]])
|
||||
_, scale, _ = quantization_util.quantize_tensor(
|
||||
inputs, axis=[1], sym=False, use_fp=True, add_scale_eps=True
|
||||
)
|
||||
self.assertAllClose(
|
||||
scale, np.array([np.finfo(np.float32).eps, np.finfo(np.float32).eps])
|
||||
)
|
||||
_, scale, _ = quantization_util.quantize_tensor(
|
||||
inputs, axis=[1], sym=False, use_fp=True, add_scale_eps=False
|
||||
)
|
||||
self.assertAllClose(scale, np.array([1.0, 1.0]))
|
||||
|
||||
|
||||
class Quantize4BTest(TestCase):
|
||||
|
||||
def test_quantize_symmetric(self):
|
||||
inputs = np.array([[1.2, 3.1, 5.5, 2.9], [0.2, -1.5, 3.3, 4.0]])
|
||||
qx, scale = quantization_util.quantize_tensor(
|
||||
inputs, axis=[1], number_bits=4
|
||||
)
|
||||
self.assertAllClose(
|
||||
qx, np.array([[2, 4, 7, 4], [0, -3, 6, 7]], dtype=np.int8)
|
||||
)
|
||||
self.assertAllClose(
|
||||
scale, np.array([0.78571427, 0.5714286], dtype=np.float32)
|
||||
)
|
||||
|
||||
def test_quantize_symmetric_with_dimension_size_one_unquantized(self):
|
||||
# inputs shape: (2, 1, 4), quantization axis 2.
|
||||
inputs = np.array([[[1.2, 3.1, 5.5, 2.9]], [[0.2, -1.5, 3.3, 4.0]]])
|
||||
qx, scale = quantization_util.quantize_tensor(
|
||||
inputs, axis=[2], number_bits=4
|
||||
)
|
||||
|
||||
self.assertAllClose(
|
||||
qx, np.array([[[2, 4, 7, 4]], [[0, -3, 6, 7]]], dtype=np.int8)
|
||||
)
|
||||
# expected scale shape: (2, 1)
|
||||
self.assertAllClose(
|
||||
scale, np.array([[0.78571427], [0.5714286]], dtype=np.float32)
|
||||
)
|
||||
|
||||
def test_quantize_asymmetric(self):
|
||||
inputs = np.array([[1.2, 3.1, 5.5, 2.9], [0.2, -1.5, 3.3, 4.0]])
|
||||
qx, scale, zp = quantization_util.quantize_tensor(
|
||||
inputs, axis=[1], sym=False, number_bits=4
|
||||
)
|
||||
|
||||
self.assertAllClose(
|
||||
qx,
|
||||
np.array([[-8, -1, 7, -2], [-3, -8, 5, 7]], dtype=np.int8),
|
||||
)
|
||||
self.assertAllClose(
|
||||
scale, np.array([0.2866667, 0.36666667], dtype=np.float32)
|
||||
)
|
||||
self.assertAllClose(
|
||||
zp, np.array([-3.4933336, -1.4333334], dtype=np.float32)
|
||||
)
|
||||
|
||||
|
||||
class QuantizationUtilTest(TestCase):
|
||||
|
||||
def test_update_to_uint4_sym(self):
|
||||
inputs = np.array([[1.2, 3.1, -5.5, 2.9], [0.2, -1.5, 3.3, 4.0]])
|
||||
qx, scale = quantization_util.quantize_tensor(
|
||||
inputs, axis=[1], sym=True, number_bits=4
|
||||
)
|
||||
dequant_from_int4 = qx * np.expand_dims(scale, -1)
|
||||
qx_n, scale_n, zp_n = quantization_util.update_to_uint4(qx, scale)
|
||||
self.assertEmpty(zp_n.shape) # A scalar numpy array.
|
||||
dequant_from_uint4 = np.expand_dims(scale_n, -1) * (qx_n - zp_n)
|
||||
np.testing.assert_allclose(dequant_from_int4, dequant_from_uint4)
|
||||
|
||||
def test_update_to_uint4_sym_combined(self):
|
||||
inputs = np.array(
|
||||
[[-1.2, 3.5, -6.2, 1.7], [1.2, 3.1, -5.5, 2.9], [0.2, -1.5, 3.3, 4.0]]
|
||||
)
|
||||
qx, scale = quantization_util.quantize_tensor(
|
||||
inputs, axis=[1], sym=True, number_bits=4
|
||||
)
|
||||
dequant_from_int4 = qx * np.expand_dims(scale, -1)
|
||||
qx_n, scale_n, zp_n = quantization_util.update_to_uint4(qx, scale)
|
||||
self.assertEqual(zp_n.shape[0], 3)
|
||||
dequant_from_uint4 = np.expand_dims(scale_n, -1) * (
|
||||
qx_n - np.expand_dims(zp_n, -1)
|
||||
)
|
||||
np.testing.assert_allclose(dequant_from_int4, dequant_from_uint4)
|
||||
|
||||
def test_update_to_uint4_asym(self):
|
||||
inputs = np.array([[1.0, 8.0, -3.0, 2.0], [-3.0, 2.0, 1.0, 8.0]])
|
||||
qx, scale, zp = quantization_util.quantize_tensor(
|
||||
inputs, axis=[1], sym=False, number_bits=4
|
||||
)
|
||||
qx_n, scale_n, zp_n = quantization_util.update_to_uint4(qx, scale, zp)
|
||||
expected_dequant = np.array([
|
||||
[0.0, 7.333333, -3.666667, 1.466667],
|
||||
[-3.666667, 1.466667, 0.0, 7.333333],
|
||||
])
|
||||
dequant_from_uint4 = np.expand_dims(scale_n, -1) * (qx_n - zp_n)
|
||||
np.testing.assert_allclose(dequant_from_uint4, expected_dequant, rtol=1e-05)
|
||||
|
||||
def test_update_to_uint4_asym_combined(self):
|
||||
inputs = np.array(
|
||||
[[1.0, 8.0, -3.0, 2.0], [-3.0, 2.0, 1.0, 8.0], [2.0, 1.0, 8.0, -3.0]]
|
||||
)
|
||||
qx, scale, zp = quantization_util.quantize_tensor(
|
||||
inputs, axis=[1], sym=False, number_bits=4
|
||||
)
|
||||
qx_n, scale_n, zp_n = quantization_util.update_to_uint4(qx, scale, zp)
|
||||
self.assertEqual(zp_n.shape[0], 3)
|
||||
expected_dequant = np.array([
|
||||
[0.0, 7.333333, -3.666667, 1.466667],
|
||||
[-3.666667, 1.466667, 0.0, 7.333333],
|
||||
[1.466667, 0.0, 7.333333, -3.666667],
|
||||
])
|
||||
dequant_from_uint4 = np.expand_dims(scale_n, -1) * (
|
||||
qx_n - np.expand_dims(zp_n, -1)
|
||||
)
|
||||
np.testing.assert_allclose(dequant_from_uint4, expected_dequant, rtol=1e-05)
|
||||
|
||||
|
||||
class QuantizeMseTest(TestCase):
|
||||
|
||||
def test_mse_reduce_precision_4bit(self):
|
||||
inputs = np.array([[-1.0, 1.0, 0.0], [1.0, 2.0, 3.0]], dtype=np.float32)
|
||||
qx, scale = quantization_util.quantize_tensor(
|
||||
inputs, axis=[1], number_bits=4, use_mse_quant=True
|
||||
)
|
||||
|
||||
# For 4 bits, maxq = 15, zero_mse_quant = (15 - 1) / 2 = 7.
|
||||
# The scale multiplier is 0.37755.
|
||||
|
||||
# For inputs row 1 ([-1.0, 1.0, 0.0]):
|
||||
# rms = sqrt(mean((-1)^2 + 1^2 + 0^2)) = sqrt(2/3) approx 0.8164966
|
||||
# scale = 0.37755 * 0.8164966 = 0.3079979
|
||||
# t_shifted(-1.0) = -1.0 / 0.3079979 + 7 = -3.2461 + 7 = 3.7539
|
||||
# -> round 4 -> qvar = -3
|
||||
# t_shifted(1.0) = 1.0 / 0.3079979 + 7 = 3.2461 + 7 = 10.2461
|
||||
# -> round 10 -> qvar = 3
|
||||
# t_shifted(0.0) = 0.0 / 0.3079979 + 7 = 7.0 -> round 7 -> qvar = 0
|
||||
|
||||
# For inputs row 2 ([1.0, 2.0, 3.0]):
|
||||
# rms = sqrt(mean(1^2 + 2^2 + 3^2)) = sqrt(14/3) approx 2.1602469
|
||||
# scale = 0.37755 * 2.1602469 = 0.8159988
|
||||
# t_shifted(1.0) = 1.0 / 0.8159988 + 7 = 1.2255 + 7 = 8.2255
|
||||
# -> round 8 -> qvar = 1
|
||||
# t_shifted(2.0) = 2.0 / 0.8159988 + 7 = 2.4509 + 7 = 9.4509
|
||||
# -> round 9 -> qvar = 2
|
||||
# t_shifted(3.0) = 3.0 / 0.8159988 + 7 = 3.6764 + 7 = 10.6764
|
||||
# -> round 11 -> qvar = 4
|
||||
self.assertAllClose(qx, np.array([[-3, 3, 0], [1, 2, 4]], dtype=np.int8))
|
||||
self.assertAllClose(scale, np.array([0.308268, 0.815601], dtype=np.float32))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
absltest.main()
|
||||
+635
@@ -0,0 +1,635 @@
|
||||
# Copyright 2024 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""CkptLoader implementation for loading the Safetensors."""
|
||||
|
||||
import array
|
||||
from typing import Iterator
|
||||
import enum
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
from typing import List, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from mediapipe.tasks.python.genai.converter import converter_base
|
||||
from mediapipe.tasks.python.genai.converter import external_dependencies
|
||||
|
||||
torch = external_dependencies.torch
|
||||
|
||||
DTYPE_MAP = {
|
||||
"F16": torch.float16,
|
||||
"BF16": torch.bfloat16,
|
||||
"F32": torch.float32,
|
||||
}
|
||||
|
||||
|
||||
class _SafetensorsShardReader:
|
||||
"""Reads a single safetensors shard."""
|
||||
|
||||
_HEAD_BYTES = 8
|
||||
|
||||
def __init__(self, shard_path: str):
|
||||
self._shard_path = shard_path
|
||||
if not os.path.exists(self._shard_path):
|
||||
raise ValueError(f"{self._shard_path} does not exists.")
|
||||
with open(self._shard_path, "rb") as f:
|
||||
head_bytes = f.read(self._HEAD_BYTES)
|
||||
metadata_bytes_num = np.frombuffer(head_bytes, dtype=np.uint64)[0]
|
||||
metadata_bytes = f.read(metadata_bytes_num)
|
||||
self.layers_info = json.loads(metadata_bytes)
|
||||
self.metadata_bytes_num = metadata_bytes_num
|
||||
|
||||
def read_tensor_as_numpy(self, tensor_name) -> np.ndarray:
|
||||
"""Reads a tensor from the model file as a numpy array with np.float32 type."""
|
||||
tensor_info = self.layers_info[tensor_name]
|
||||
with open(self._shard_path, "rb") as f:
|
||||
shape = tensor_info["shape"]
|
||||
dtype = tensor_info["dtype"]
|
||||
if dtype not in DTYPE_MAP:
|
||||
raise ValueError(f"{dtype} is not supported.")
|
||||
data_offsets = tensor_info["data_offsets"]
|
||||
f.seek(int(self._HEAD_BYTES + self.metadata_bytes_num + data_offsets[0]))
|
||||
tensor_bytes = f.read(data_offsets[1] - data_offsets[0])
|
||||
raw_tensor = torch.frombuffer(
|
||||
array.array("b", tensor_bytes), dtype=DTYPE_MAP[dtype]
|
||||
).reshape(shape)
|
||||
return raw_tensor.float().t().contiguous().numpy()
|
||||
|
||||
def get_tensor_names(self) -> List[str]:
|
||||
names = list(self.layers_info.keys())
|
||||
if "__metadata__" in names:
|
||||
names.remove("__metadata__")
|
||||
return names
|
||||
|
||||
|
||||
class _SafetensorsReader:
|
||||
"""Reads all the safetensors shards."""
|
||||
|
||||
def __init__(self, ckpt_path: str):
|
||||
shards = []
|
||||
if os.path.isdir(ckpt_path):
|
||||
# Read all safetensors files within checkpoint
|
||||
for shard_path in glob.glob(os.path.join(ckpt_path, "*.safetensors")):
|
||||
shards.append(_SafetensorsShardReader(shard_path))
|
||||
else:
|
||||
# Assume the ckpt_path is a file or a file pattern to match.
|
||||
for shard_path in glob.glob(ckpt_path):
|
||||
shards.append(_SafetensorsShardReader(shard_path))
|
||||
assert shards is not None
|
||||
|
||||
self._ckpt_path = ckpt_path
|
||||
self._tensors_map = {}
|
||||
for shard in shards:
|
||||
tensor_names = shard.get_tensor_names()
|
||||
for tensor_name in tensor_names:
|
||||
if tensor_name in self._tensors_map:
|
||||
raise ValueError(f"Duplicate tensor name: {tensor_name}")
|
||||
self._tensors_map[tensor_name] = shard
|
||||
|
||||
def get_tensor_names(self) -> List[str]:
|
||||
return list(self._tensors_map.keys())
|
||||
|
||||
def read_tensor_as_numpy(self, tensor_name: str) -> np.ndarray:
|
||||
return self._tensors_map[tensor_name].read_tensor_as_numpy(tensor_name)
|
||||
|
||||
|
||||
class LayerType(enum.Enum):
|
||||
"""Enum for layer type."""
|
||||
|
||||
NONE = 0
|
||||
ATTENTION = 1 # Layer is part of the attention module.
|
||||
FEEDFORWARD = 2 # Layer is part of the feedforward module in the Transformer.
|
||||
EMBEDDING = 3 # Layer is the embedding lookup or final projection layer.
|
||||
LAYER_NORM = (
|
||||
4 # Layer is layer normalization before and after attention layer.
|
||||
)
|
||||
LORA = 5 # Layer is LoRA weights augmented on the base model layers.
|
||||
SKIP = 6 # Log a warning, and remove this not-yet-supported layer entirely.
|
||||
|
||||
@classmethod
|
||||
def get_layer_type(cls, layer_name: str):
|
||||
"""Gets the layer type of the given layer name."""
|
||||
ffn_layers = [
|
||||
"mlp",
|
||||
]
|
||||
attn_layers = [
|
||||
"self_attn",
|
||||
]
|
||||
emb_layers = [
|
||||
"embed_tokens",
|
||||
"embedder",
|
||||
"lm_head",
|
||||
]
|
||||
layer_norms = [
|
||||
"input_layernorm",
|
||||
"post_attention_layernorm",
|
||||
"final_layernorm",
|
||||
"model.norm.weight",
|
||||
"pre_feedforward_layernorm",
|
||||
"post_feedforward_layernorm",
|
||||
]
|
||||
lora_layers = ["lora"]
|
||||
skip_layers = [
|
||||
"multi_modal_projector",
|
||||
"vision_tower",
|
||||
]
|
||||
if any(sub_name in layer_name for sub_name in skip_layers):
|
||||
return LayerType.SKIP
|
||||
if any(sub_name in layer_name for sub_name in lora_layers):
|
||||
return LayerType.LORA
|
||||
if any(sub_name in layer_name for sub_name in attn_layers):
|
||||
return LayerType.ATTENTION
|
||||
if any(sub_name in layer_name for sub_name in ffn_layers):
|
||||
return LayerType.FEEDFORWARD
|
||||
if any(sub_name in layer_name for sub_name in emb_layers):
|
||||
return LayerType.EMBEDDING
|
||||
if any(sub_name in layer_name for sub_name in layer_norms):
|
||||
return LayerType.LAYER_NORM
|
||||
else:
|
||||
return LayerType.NONE
|
||||
|
||||
|
||||
class StablelmMapper(converter_base.LayerActionMapperBase):
|
||||
"""LayerActionMapper for handling the StableLM model."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
is_symmetric: bool,
|
||||
attention_quant_bits: int,
|
||||
feedforward_quant_bits: int,
|
||||
embedding_quant_bits: int,
|
||||
backend: str,
|
||||
reader: _SafetensorsReader,
|
||||
):
|
||||
super().__init__(
|
||||
is_symmetric=is_symmetric,
|
||||
attention_quant_bits=attention_quant_bits,
|
||||
feedforward_quant_bits=feedforward_quant_bits,
|
||||
embedding_quant_bits=embedding_quant_bits,
|
||||
backend=backend,
|
||||
)
|
||||
self._reader = reader
|
||||
|
||||
def map_to_actions(
|
||||
self, layer_name: str
|
||||
) -> Optional[List[converter_base.QuantizationAction]]:
|
||||
"""Map the given layer name to actions."""
|
||||
tensor_value = self._reader.read_tensor_as_numpy(layer_name)
|
||||
quantize_axis = None
|
||||
quantize_bits = None
|
||||
layer_type = LayerType.get_layer_type(layer_name)
|
||||
|
||||
if layer_type != LayerType.LAYER_NORM and layer_name.endswith(".weight"):
|
||||
quantize_axis = [0]
|
||||
if layer_type == LayerType.FEEDFORWARD:
|
||||
quantize_bits = self._feedforward_quant_bits
|
||||
elif layer_type == LayerType.ATTENTION:
|
||||
quantize_bits = self._attention_quant_bits
|
||||
if self._backend == "cpu" and ".o_proj." in layer_name:
|
||||
tensor_value = np.transpose(tensor_value)
|
||||
quantize_axis = [1]
|
||||
elif layer_type == LayerType.EMBEDDING:
|
||||
quantize_bits = self._embedding_quant_bits
|
||||
if self._backend == "cpu" and ".embed_tokens." in layer_name:
|
||||
tensor_value = np.transpose(tensor_value)
|
||||
quantize_axis = [1]
|
||||
target_name = self.update_target_name(layer_name)
|
||||
|
||||
actions = [
|
||||
converter_base.QuantizationAction(
|
||||
tensor_name=layer_name,
|
||||
tensor_value=tensor_value,
|
||||
target_name=target_name,
|
||||
quantize_axis=quantize_axis,
|
||||
quantize_bits=quantize_bits,
|
||||
pack_dim=0,
|
||||
)
|
||||
]
|
||||
return actions
|
||||
|
||||
def update_target_name(self, target_name: str) -> str:
|
||||
"""Updates the target name to match the tensor name convention."""
|
||||
target_name = target_name.replace(
|
||||
"model.layers.", "params.lm.transformer.x_layers_"
|
||||
)
|
||||
target_name = target_name.replace("mlp.up_proj", "ff_layer.ffn_layer1")
|
||||
target_name = target_name.replace("mlp.down_proj", "ff_layer.ffn_layer2")
|
||||
target_name = target_name.replace(
|
||||
"mlp.gate_proj", "ff_layer.ffn_layer1_gate"
|
||||
)
|
||||
target_name = target_name.replace("input_layernorm", "pre_layer_norm")
|
||||
target_name = target_name.replace(
|
||||
"pre_layer_norm.weight", "pre_layer_norm.scale"
|
||||
)
|
||||
if self._backend == "cpu":
|
||||
target_name = target_name.replace(
|
||||
"post_attention_layernorm", "ff_layer.pre_layer_norm"
|
||||
)
|
||||
target_name = target_name.replace(
|
||||
"ff_layer.pre_layer_norm.weight", "ff_layer.pre_layer_norm.scale"
|
||||
)
|
||||
else:
|
||||
target_name = target_name.replace(
|
||||
"post_attention_layernorm", "post_layer_norm"
|
||||
)
|
||||
target_name = target_name.replace(
|
||||
"post_layer_norm.weight", "post_layer_norm.scale"
|
||||
)
|
||||
target_name = target_name.replace("self_attn.q_proj", "self_attention.q")
|
||||
target_name = target_name.replace("self_attn.k_proj", "self_attention.k")
|
||||
target_name = target_name.replace("self_attn.v_proj", "self_attention.v")
|
||||
target_name = target_name.replace("self_attn.o_proj", "self_attention.post")
|
||||
target_name = target_name.replace(
|
||||
"model.embed_tokens", "params.lm.token_embedding"
|
||||
)
|
||||
target_name = target_name.replace("model.norm", "params.lm.final_ln")
|
||||
target_name = target_name.replace("final_ln.weight", "final_ln.scale")
|
||||
target_name = target_name.replace("lm_head", "params.lm.softmax.logits_ffn")
|
||||
target_name = target_name.replace(".weight", ".w")
|
||||
|
||||
return target_name
|
||||
|
||||
|
||||
class PhiMapper(converter_base.LayerActionMapperBase):
|
||||
"""LayerActionMapper for handling the Phi model."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
is_symmetric: bool,
|
||||
attention_quant_bits: int,
|
||||
feedforward_quant_bits: int,
|
||||
embedding_quant_bits: int,
|
||||
backend: str,
|
||||
reader: _SafetensorsReader,
|
||||
):
|
||||
super().__init__(
|
||||
is_symmetric=is_symmetric,
|
||||
attention_quant_bits=attention_quant_bits,
|
||||
feedforward_quant_bits=feedforward_quant_bits,
|
||||
embedding_quant_bits=embedding_quant_bits,
|
||||
backend=backend,
|
||||
)
|
||||
self._reader = reader
|
||||
|
||||
def map_to_actions(
|
||||
self, layer_name: str
|
||||
) -> Optional[List[converter_base.QuantizationAction]]:
|
||||
"""Map the given layer name to actions."""
|
||||
tensor_value = self._reader.read_tensor_as_numpy(layer_name)
|
||||
quantize_axis = None
|
||||
quantize_bits = None
|
||||
layer_type = LayerType.get_layer_type(layer_name)
|
||||
|
||||
if (
|
||||
layer_type != LayerType.LAYER_NORM
|
||||
and layer_name.endswith(".weight")
|
||||
and layer_type != LayerType.LORA
|
||||
):
|
||||
quantize_axis = [0]
|
||||
if layer_type == LayerType.FEEDFORWARD:
|
||||
quantize_bits = self._feedforward_quant_bits
|
||||
elif layer_type == LayerType.ATTENTION:
|
||||
quantize_bits = self._attention_quant_bits
|
||||
if self._backend == "cpu" and ".dense." in layer_name:
|
||||
tensor_value = np.transpose(tensor_value)
|
||||
quantize_axis = [1]
|
||||
elif layer_type == LayerType.EMBEDDING:
|
||||
quantize_bits = self._embedding_quant_bits
|
||||
if self._backend == "cpu" and ".embed_tokens." in layer_name:
|
||||
tensor_value = np.transpose(tensor_value)
|
||||
quantize_axis = [1]
|
||||
target_name = self.update_target_name(layer_name)
|
||||
|
||||
actions = [
|
||||
converter_base.QuantizationAction(
|
||||
tensor_name=layer_name,
|
||||
tensor_value=tensor_value,
|
||||
target_name=target_name,
|
||||
quantize_axis=quantize_axis,
|
||||
quantize_bits=quantize_bits,
|
||||
pack_dim=0,
|
||||
)
|
||||
]
|
||||
return actions
|
||||
|
||||
def update_target_name(self, target_name: str) -> str:
|
||||
"""Updates the target name to match the tensor name convention."""
|
||||
target_name = target_name.replace("base_model.model.", "")
|
||||
target_name = target_name.replace(
|
||||
"model.layers.", "params.lm.transformer.x_layers_"
|
||||
)
|
||||
|
||||
layer_type = LayerType.get_layer_type(target_name)
|
||||
if layer_type == LayerType.FEEDFORWARD:
|
||||
target_name = target_name.replace(".weight", ".linear.w")
|
||||
target_name = target_name.replace(".bias", ".bias.b")
|
||||
target_name = target_name.replace("mlp.fc1", "ff_layer.ffn_layer1")
|
||||
target_name = target_name.replace("mlp.fc2", "ff_layer.ffn_layer2")
|
||||
|
||||
elif layer_type == LayerType.ATTENTION:
|
||||
target_name = target_name.replace(".weight", ".linear.w")
|
||||
target_name = target_name.replace(".bias", ".bias.b")
|
||||
target_name = target_name.replace("self_attn.q_proj", "self_attention.q")
|
||||
target_name = target_name.replace("self_attn.k_proj", "self_attention.k")
|
||||
target_name = target_name.replace("self_attn.v_proj", "self_attention.v")
|
||||
target_name = target_name.replace(
|
||||
"self_attn.dense", "self_attention.post"
|
||||
)
|
||||
elif layer_type == LayerType.EMBEDDING:
|
||||
target_name = target_name.replace(
|
||||
"model.embed_tokens", "params.lm.token_embedding"
|
||||
)
|
||||
target_name = target_name.replace(
|
||||
"lm_head", "params.lm.softmax.logits_ffn"
|
||||
)
|
||||
target_name = target_name.replace(
|
||||
"logits_ffn.weight", "logits_ffn.linear.w"
|
||||
)
|
||||
target_name = target_name.replace("logits_ffn.bias", "logits_ffn.bias.b")
|
||||
elif layer_type == LayerType.LAYER_NORM:
|
||||
target_name = target_name.replace("input_layernorm", "pre_layer_norm")
|
||||
target_name = target_name.replace(
|
||||
"pre_layer_norm.weight", "pre_layer_norm.scale"
|
||||
)
|
||||
target_name = target_name.replace(
|
||||
"model.final_layernorm", "params.lm.final_ln"
|
||||
)
|
||||
target_name = target_name.replace("final_ln.weight", "final_ln.scale")
|
||||
target_name = target_name.replace(".weight", ".w")
|
||||
|
||||
# For LoRA weights
|
||||
if "post" in target_name:
|
||||
target_name = target_name.replace("lora_A.linear.w", "w_prime_right")
|
||||
target_name = target_name.replace("lora_B.linear.w", "w_prime_left")
|
||||
else:
|
||||
target_name = target_name.replace("lora_A.linear.w", "w_prime_left")
|
||||
target_name = target_name.replace("lora_B.linear.w", "w_prime_right")
|
||||
|
||||
return target_name
|
||||
|
||||
|
||||
class GemmaMapper(converter_base.LayerActionMapperBase):
|
||||
"""LayerActionMapper for handling the Gemma model."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
is_symmetric: bool,
|
||||
attention_quant_bits: int,
|
||||
feedforward_quant_bits: int,
|
||||
embedding_quant_bits: int,
|
||||
backend: str,
|
||||
reader: _SafetensorsReader,
|
||||
is_v2: bool,
|
||||
):
|
||||
super().__init__(
|
||||
is_symmetric=is_symmetric,
|
||||
attention_quant_bits=attention_quant_bits,
|
||||
feedforward_quant_bits=feedforward_quant_bits,
|
||||
embedding_quant_bits=embedding_quant_bits,
|
||||
backend=backend,
|
||||
)
|
||||
self._reader = reader
|
||||
self._is_v2 = is_v2
|
||||
|
||||
def map_to_actions(
|
||||
self, layer_name: str
|
||||
) -> Optional[List[converter_base.QuantizationAction]]:
|
||||
"""Map the given layer name to actions."""
|
||||
|
||||
layer_type = LayerType.get_layer_type(layer_name)
|
||||
if layer_type == LayerType.SKIP:
|
||||
print("Unsupported Gemma layer: %s, skipping." % layer_name)
|
||||
return
|
||||
|
||||
tensor_value = self._reader.read_tensor_as_numpy(layer_name)
|
||||
quantize_axis = None
|
||||
quantize_bits = None
|
||||
|
||||
if (
|
||||
layer_type != LayerType.LAYER_NORM
|
||||
and layer_name.endswith(".weight")
|
||||
and layer_type != LayerType.LORA
|
||||
):
|
||||
quantize_axis = [0]
|
||||
if layer_type == LayerType.FEEDFORWARD:
|
||||
quantize_bits = self._feedforward_quant_bits
|
||||
elif layer_type == LayerType.ATTENTION:
|
||||
quantize_bits = self._attention_quant_bits
|
||||
if "o_proj" in layer_name:
|
||||
tensor_value = np.transpose(tensor_value)
|
||||
quantize_axis = [1]
|
||||
if "q_norm" in layer_name:
|
||||
# TODO: Look into quantization for q_norm and k_norm
|
||||
quantize_axis = None
|
||||
quantize_bits = None
|
||||
if "k_norm" in layer_name:
|
||||
quantize_axis = None
|
||||
quantize_bits = None
|
||||
elif layer_type == LayerType.EMBEDDING:
|
||||
quantize_bits = self._embedding_quant_bits
|
||||
|
||||
target_name = self.update_target_name(layer_name)
|
||||
|
||||
actions = [
|
||||
converter_base.QuantizationAction(
|
||||
tensor_name=layer_name,
|
||||
tensor_value=tensor_value,
|
||||
target_name=target_name,
|
||||
quantize_axis=quantize_axis,
|
||||
quantize_bits=quantize_bits,
|
||||
pack_dim=0,
|
||||
)
|
||||
]
|
||||
return actions
|
||||
|
||||
def update_target_name(self, target_name: str) -> str:
|
||||
"""Updates the target name to match the tensor name convention."""
|
||||
|
||||
# For removing multimodality stack from Gemma3-4B
|
||||
target_name = target_name.replace("language_model.", "")
|
||||
|
||||
target_name = target_name.replace("base_model.model.", "")
|
||||
target_name = target_name.replace(
|
||||
"model.layers.", "params.lm.transformer.x_layers_"
|
||||
)
|
||||
target_name = target_name.replace("mlp.up_proj", "ff_layer.ffn_layer1")
|
||||
target_name = target_name.replace("mlp.down_proj", "ff_layer.ffn_layer2")
|
||||
target_name = target_name.replace(
|
||||
"mlp.gate_proj", "ff_layer.ffn_layer1_gate"
|
||||
)
|
||||
target_name = target_name.replace("input_layernorm", "pre_layer_norm")
|
||||
target_name = target_name.replace(
|
||||
"pre_layer_norm.weight", "pre_layer_norm.scale"
|
||||
)
|
||||
|
||||
# Gemma and Gemma2/Gemma3 differ slightly in their use of the
|
||||
# "post_attention_layernorm" tensor name.
|
||||
if self._is_v2:
|
||||
target_name = target_name.replace(
|
||||
"post_attention_layernorm", "post_layer_norm"
|
||||
)
|
||||
else:
|
||||
target_name = target_name.replace(
|
||||
"post_attention_layernorm", "ff_layer.pre_layer_norm"
|
||||
)
|
||||
|
||||
target_name = target_name.replace(
|
||||
"pre_feedforward_layernorm", "ff_layer.pre_layer_norm"
|
||||
)
|
||||
target_name = target_name.replace(
|
||||
"post_feedforward_layernorm", "ff_layer.post_layer_norm"
|
||||
)
|
||||
target_name = target_name.replace(
|
||||
"ff_layer.pre_layer_norm.weight", "ff_layer.pre_layer_norm.scale"
|
||||
)
|
||||
target_name = target_name.replace(
|
||||
"ff_layer.post_layer_norm.weight", "ff_layer.post_layer_norm.scale"
|
||||
)
|
||||
target_name = target_name.replace(
|
||||
"post_layer_norm.weight", "post_layer_norm.scale"
|
||||
)
|
||||
target_name = target_name.replace("self_attn.q_proj", "self_attention.q")
|
||||
target_name = target_name.replace("self_attn.k_proj", "self_attention.k")
|
||||
target_name = target_name.replace("self_attn.v_proj", "self_attention.v")
|
||||
target_name = target_name.replace("self_attn.o_proj", "self_attention.post")
|
||||
|
||||
# Gemma3 QK norms
|
||||
target_name = target_name.replace(
|
||||
"self_attn.q_norm", "self_attention.q_norm"
|
||||
)
|
||||
target_name = target_name.replace(
|
||||
"self_attention.q_norm.weight", "self_attention.q_norm.scale"
|
||||
)
|
||||
target_name = target_name.replace(
|
||||
"self_attn.k_norm", "self_attention.k_norm"
|
||||
)
|
||||
target_name = target_name.replace(
|
||||
"self_attention.k_norm.weight", "self_attention.k_norm.scale"
|
||||
)
|
||||
|
||||
target_name = target_name.replace(
|
||||
"model.embed_tokens", "params.lm.softmax.logits_ffn"
|
||||
)
|
||||
# Alternative Gemma3 embedding layer name
|
||||
target_name = target_name.replace(
|
||||
"embedder", "params.lm.softmax.logits_ffn"
|
||||
)
|
||||
|
||||
target_name = target_name.replace("model.norm", "params.lm.final_ln")
|
||||
target_name = target_name.replace("final_ln.weight", "final_ln.scale")
|
||||
target_name = target_name.replace(".weight", ".w")
|
||||
|
||||
# For LoRA weights
|
||||
if "post" in target_name:
|
||||
target_name = target_name.replace("lora_A.w", "w_prime_right")
|
||||
target_name = target_name.replace("lora_B.w", "w_prime_left")
|
||||
else:
|
||||
target_name = target_name.replace("lora_A.w", "w_prime_left")
|
||||
target_name = target_name.replace("lora_B.w", "w_prime_right")
|
||||
|
||||
return target_name
|
||||
|
||||
|
||||
class SafetensorsCkptLoader(converter_base.CkptLoaderBase):
|
||||
"""CkptLoader implementation for loading the Safetensors."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ckpt_path: str,
|
||||
is_symmetric: bool,
|
||||
attention_quant_bits: int,
|
||||
feedforward_quant_bits: int,
|
||||
embedding_quant_bits: int,
|
||||
special_model: str,
|
||||
backend: str,
|
||||
):
|
||||
"""Initializes the loader.
|
||||
|
||||
Args:
|
||||
ckpt_path: The filepath to the safetensors file.
|
||||
is_symmetric: Whether to apply symmetric or asymmetric quantization.
|
||||
attention_quant_bits: An integer that specify the target quantization bits
|
||||
(support 8 or 4) for the attention layers.
|
||||
feedforward_quant_bits: An integer that specify the target quantization
|
||||
bits (support 8 or 4) for the feedforward layers in each Transformer
|
||||
blocks.
|
||||
embedding_quant_bits: An integer that specify the target quantization bits
|
||||
(support 8 or 4) for the embedding (and the final projection) layers.
|
||||
special_model: A string that indicates which input model is and whether
|
||||
any special treatment is needed.
|
||||
backend: A string indicating the backend used when converting this model.
|
||||
Valid options are "cpu" and "gpu".
|
||||
"""
|
||||
super().__init__(
|
||||
ckpt_path,
|
||||
is_symmetric,
|
||||
attention_quant_bits,
|
||||
feedforward_quant_bits,
|
||||
embedding_quant_bits,
|
||||
)
|
||||
|
||||
self._special_model = special_model
|
||||
self._reader = _SafetensorsReader(ckpt_path)
|
||||
if special_model in ["STABLELM_4E1T_3B"]:
|
||||
self.mapper = StablelmMapper(
|
||||
is_symmetric,
|
||||
attention_quant_bits,
|
||||
feedforward_quant_bits,
|
||||
embedding_quant_bits,
|
||||
backend,
|
||||
self._reader,
|
||||
)
|
||||
elif special_model in ["PHI_2"]:
|
||||
self.mapper = PhiMapper(
|
||||
is_symmetric,
|
||||
attention_quant_bits,
|
||||
feedforward_quant_bits,
|
||||
embedding_quant_bits,
|
||||
backend,
|
||||
self._reader,
|
||||
)
|
||||
elif special_model in [
|
||||
"GEMMA_2B",
|
||||
"GEMMA_7B",
|
||||
"GEMMA2_2B",
|
||||
"GEMMA3_1B",
|
||||
"GEMMA3_4B",
|
||||
"GEMMA3_12B",
|
||||
"GEMMA3_27B",
|
||||
"GEMMA3_300M",
|
||||
"FUNCTIONGEMMA3_300M",
|
||||
]:
|
||||
self.mapper = GemmaMapper(
|
||||
is_symmetric,
|
||||
attention_quant_bits,
|
||||
feedforward_quant_bits,
|
||||
embedding_quant_bits,
|
||||
backend,
|
||||
self._reader,
|
||||
False if special_model in ["GEMMA_2B", "GEMMA_7B"] else True,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unknown special model: {special_model}")
|
||||
|
||||
def load_to_actions(
|
||||
self,
|
||||
) -> Iterator[List[converter_base.QuantizationAction]]:
|
||||
tensor_names = self._reader.get_tensor_names()
|
||||
for tensor_name in tensor_names:
|
||||
tensor_actions = self.mapper.map_to_actions(tensor_name)
|
||||
if tensor_actions is None:
|
||||
continue
|
||||
yield tensor_actions
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
# Copyright 2024 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Unit tests for safetensors_converter."""
|
||||
|
||||
import os
|
||||
|
||||
from absl.testing import absltest
|
||||
from absl.testing import parameterized
|
||||
|
||||
from mediapipe.tasks.python.genai.converter import safetensors_converter
|
||||
from mediapipe.tasks.python.test import test_utils
|
||||
|
||||
_TEST_DATA_DIR = 'mediapipe/tasks/testdata/text'
|
||||
_SAFETENSORS_FILE = test_utils.get_test_data_path(
|
||||
os.path.join(_TEST_DATA_DIR, 'stablelm_3b_4e1t_test_weight.safetensors')
|
||||
)
|
||||
|
||||
|
||||
class SafetensorsConverterTest(parameterized.TestCase):
|
||||
VARIABLE_NAMES = [
|
||||
'model.embed_tokens.weight',
|
||||
'model.layers.0.input_layernorm.bias',
|
||||
'model.layers.0.input_layernorm.weight',
|
||||
'model.layers.0.mlp.down_proj.weight',
|
||||
'model.layers.0.mlp.gate_proj.weight',
|
||||
'model.layers.0.mlp.up_proj.weight',
|
||||
'model.layers.0.post_attention_layernorm.bias',
|
||||
'model.layers.0.post_attention_layernorm.weight',
|
||||
'model.layers.0.self_attn.k_proj.weight',
|
||||
'model.layers.0.self_attn.o_proj.weight',
|
||||
'model.layers.0.self_attn.q_proj.weight',
|
||||
'model.layers.0.self_attn.v_proj.weight',
|
||||
'model.norm.bias',
|
||||
'model.norm.weight',
|
||||
'lm_head.weight',
|
||||
]
|
||||
|
||||
def test_init(self):
|
||||
loader = safetensors_converter.SafetensorsCkptLoader(
|
||||
ckpt_path=_SAFETENSORS_FILE,
|
||||
is_symmetric=True,
|
||||
attention_quant_bits=8,
|
||||
feedforward_quant_bits=8,
|
||||
embedding_quant_bits=8,
|
||||
special_model='STABLELM_4E1T_3B',
|
||||
backend='gpu',
|
||||
)
|
||||
self.assertEqual(loader._ckpt_path, _SAFETENSORS_FILE)
|
||||
self.assertEqual(loader._is_symmetric, True)
|
||||
self.assertEqual(loader._attention_quant_bits, 8)
|
||||
self.assertEqual(loader._feedforward_quant_bits, 8)
|
||||
|
||||
@parameterized.product(
|
||||
quant_bits=(4, 8),
|
||||
)
|
||||
def test_load_to_actions(self, quant_bits):
|
||||
loader = safetensors_converter.SafetensorsCkptLoader(
|
||||
ckpt_path=_SAFETENSORS_FILE,
|
||||
is_symmetric=True,
|
||||
attention_quant_bits=8,
|
||||
feedforward_quant_bits=quant_bits,
|
||||
embedding_quant_bits=8,
|
||||
special_model='STABLELM_4E1T_3B',
|
||||
backend='gpu',
|
||||
)
|
||||
actions = loader.load_to_actions()
|
||||
self.assertLen(list(actions), 15)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
absltest.main()
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
# Copyright 2024 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""ModelWriter for writing a set of weights as binary files."""
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
from typing import Dict, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
from mediapipe.tasks.python.genai.converter import converter_base
|
||||
from mediapipe.tasks.python.genai.converter import external_dependencies
|
||||
from mediapipe.tasks.python.genai.converter import quantization_util
|
||||
|
||||
jnp = external_dependencies.jnp
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def filemanager(filename: str, mode: str):
|
||||
try:
|
||||
with open(filename, mode) as f:
|
||||
yield f
|
||||
finally:
|
||||
pass
|
||||
|
||||
|
||||
def removeprefix(s, prefix):
|
||||
"""Removes the prefix from a string."""
|
||||
if s.startswith(prefix):
|
||||
return s[len(prefix) :]
|
||||
return s
|
||||
|
||||
|
||||
class WeightBinsWriter(converter_base.ModelWriterBase):
|
||||
"""A ModelWriter for writing a set of weights as binary files."""
|
||||
|
||||
def get_weight_info(self, var_name: str, weight: np.ndarray) -> str:
|
||||
"""Gets the string that describes the weights."""
|
||||
dtype_str = str(weight.dtype)
|
||||
shape_str = '_'.join(map(str, weight.shape))
|
||||
return f'mdl_vars.{var_name}.{dtype_str}.{shape_str}\n'
|
||||
|
||||
def write_variables(
|
||||
self,
|
||||
variables: Dict[str, Tuple[np.ndarray, bool]],
|
||||
use_fake_values: bool = False,
|
||||
):
|
||||
"""Writes variable to the binary files. One for each layer.
|
||||
|
||||
Args:
|
||||
variables: A dictionary that maps from the target variable names to the
|
||||
quantized tensor values along with a boolean that indicates whether to
|
||||
pack the values (only applicable for the 4-bit quantized tensors).
|
||||
use_fake_values: Whether to use fake values for the weights. If set to
|
||||
True, the weights will be filled with zeros.
|
||||
"""
|
||||
weights_info = []
|
||||
for var_name, value in variables.items():
|
||||
output = value[0]
|
||||
if use_fake_values:
|
||||
output.fill(0)
|
||||
if output.dtype == jnp.int2:
|
||||
print('saving int2 weight: ', var_name)
|
||||
output = output.astype(jnp.int8)
|
||||
output = np.expand_dims(np.ravel(output), axis=-1)
|
||||
output = quantization_util.pack_2bit_into_int8(output, 0)
|
||||
if value[1]:
|
||||
# Squeeze the tensor to make sure it is a 1D array for packing.
|
||||
output = np.expand_dims(np.ravel(output), axis=-1)
|
||||
# Extra pack needed for 4 bit. We always pack the weights along the
|
||||
# first dimension since the tensor has already been squeezed.
|
||||
output = quantization_util.pack_4bit(output, 0, jnp.int8)
|
||||
if 'combined_qkv' in var_name:
|
||||
var_name = removeprefix(var_name, 'mld_vars.')
|
||||
var_name_q = var_name.replace('combined_qkv', 'q')
|
||||
var_name_k = var_name.replace('combined_qkv', 'k')
|
||||
var_name_v = var_name.replace('combined_qkv', 'v')
|
||||
if output.shape[0] == 3:
|
||||
weight_q, weight_k, weight_v = output
|
||||
assert weight_q.shape == weight_k.shape == weight_v.shape
|
||||
else: # LoRA right weight is shared across q, k, v
|
||||
weight_q = weight_k = weight_v = output
|
||||
weights_info.append(self.get_weight_info(var_name_q, weight_q))
|
||||
path_q = os.path.join(self._output_dir, var_name_q)
|
||||
with filemanager(path_q, 'wb') as f:
|
||||
f.write(weight_q.tobytes())
|
||||
weights_info.append(self.get_weight_info(var_name_k, weight_k))
|
||||
path_k = os.path.join(self._output_dir, var_name_k)
|
||||
with filemanager(path_k, 'wb') as f:
|
||||
f.write(weight_k.tobytes())
|
||||
path_v = os.path.join(self._output_dir, var_name_v)
|
||||
with filemanager(path_v, 'wb') as f:
|
||||
f.write(weight_v.tobytes())
|
||||
weights_info.append(self.get_weight_info(var_name_v, weight_v))
|
||||
else:
|
||||
if 'key' in var_name:
|
||||
var_name = var_name.replace('key', 'k')
|
||||
if 'query' in var_name:
|
||||
var_name = var_name.replace('query', 'q')
|
||||
if 'value' in var_name:
|
||||
var_name = var_name.replace('value', 'v')
|
||||
path = os.path.join(
|
||||
self._output_dir, removeprefix(var_name, 'mdl_vars.')
|
||||
)
|
||||
with filemanager(path, 'wb') as f:
|
||||
f.write(output.tobytes())
|
||||
weights_info.append(self.get_weight_info(var_name, output))
|
||||
|
||||
# Sort weights_info
|
||||
weights_info.sort()
|
||||
with filemanager(
|
||||
os.path.join(self._output_dir, 'layer_info.txt'), 'a'
|
||||
) as finfo:
|
||||
for line in weights_info:
|
||||
finfo.write(line + '\n')
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
# Copyright 2024 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Unit tests for pax_converter."""
|
||||
|
||||
import os
|
||||
|
||||
from absl import flags
|
||||
from absl.testing import absltest
|
||||
from absl.testing import parameterized
|
||||
import numpy as np
|
||||
|
||||
from mediapipe.tasks.python.genai.converter import weight_bins_writer
|
||||
|
||||
|
||||
class WeightBinsWriterTest(parameterized.TestCase):
|
||||
|
||||
def test_get_weight_info(self):
|
||||
output_dir = os.path.join(flags.FLAGS.test_tmpdir, 'output_dir')
|
||||
writer = weight_bins_writer.WeightBinsWriter(
|
||||
output_dir=output_dir, backend='cpu'
|
||||
)
|
||||
var_name = 'params.lm.softmax.logits_ffn.linear.w'
|
||||
weight_info = writer.get_weight_info(
|
||||
var_name, np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32)
|
||||
)
|
||||
self.assertEqual(
|
||||
weight_info,
|
||||
'mdl_vars.params.lm.softmax.logits_ffn.linear.w.float32.2_3\n',
|
||||
)
|
||||
|
||||
def test_load_to_actions(self):
|
||||
output_dir = os.path.join(flags.FLAGS.test_tmpdir, 'output_dir')
|
||||
writer = weight_bins_writer.WeightBinsWriter(
|
||||
output_dir=output_dir, backend='cpu'
|
||||
)
|
||||
variables = {
|
||||
'mdl_vars.params.lm.softmax.logits_ffn.linear.w': (
|
||||
np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32),
|
||||
False,
|
||||
),
|
||||
}
|
||||
writer.write_variables(variables)
|
||||
file_size = os.path.getsize(
|
||||
os.path.join(output_dir, 'params.lm.softmax.logits_ffn.linear.w')
|
||||
)
|
||||
self.assertEqual(file_size, 6 * 4)
|
||||
|
||||
@parameterized.named_parameters(
|
||||
(
|
||||
'real_weights',
|
||||
np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32),
|
||||
False,
|
||||
),
|
||||
(
|
||||
'fake_weights',
|
||||
np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32),
|
||||
True,
|
||||
),
|
||||
)
|
||||
def test_write_variables(self, var_values, use_fake_values):
|
||||
output_dir = os.path.join(flags.FLAGS.test_tmpdir, 'output_dir')
|
||||
writer = weight_bins_writer.WeightBinsWriter(
|
||||
output_dir=output_dir, backend='gpu'
|
||||
)
|
||||
variables = {
|
||||
'mdl_vars.params.lm.softmax.logits_ffn.linear.w': (
|
||||
var_values,
|
||||
False,
|
||||
),
|
||||
}
|
||||
writer.write_variables(variables, use_fake_values=use_fake_values)
|
||||
with open(
|
||||
os.path.join(output_dir, 'params.lm.softmax.logits_ffn.linear.w'), 'rb'
|
||||
) as f:
|
||||
data = np.frombuffer(f.read(), dtype=np.float32).reshape(var_values.shape)
|
||||
expected_values = var_values if not use_fake_values else np.zeros_like(
|
||||
var_values
|
||||
)
|
||||
self.assertTrue(np.all(data == expected_values))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
absltest.main()
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
File diff suppressed because it is too large
Load Diff
+34
@@ -0,0 +1,34 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""CLI tool for display metadata."""
|
||||
|
||||
from absl import app
|
||||
from absl import flags
|
||||
|
||||
from mediapipe.tasks.python.metadata import metadata
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_string('model_path', None, 'Path to the TFLite model file.')
|
||||
flags.DEFINE_string('export_json_path', None, 'Path to the output JSON file.')
|
||||
|
||||
|
||||
def main(_):
|
||||
displayer = metadata.MetadataDisplayer.with_model_file(FLAGS.model_path)
|
||||
with open(FLAGS.export_json_path, 'w') as f:
|
||||
f.write(displayer.get_metadata_json())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(main)
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Writes metadata and label file to the image classifier models."""
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from mediapipe.tasks.python.metadata.metadata_writers import metadata_writer
|
||||
|
||||
_MODEL_NAME = "ImageClassifier"
|
||||
_MODEL_DESCRIPTION = ("Identify the most prominent object in the image from a "
|
||||
"known set of categories.")
|
||||
|
||||
|
||||
class MetadataWriter(metadata_writer.MetadataWriterBase):
|
||||
"""MetadataWriter to write the metadata for image classifier."""
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
model_buffer: bytearray,
|
||||
input_norm_mean: List[float],
|
||||
input_norm_std: List[float],
|
||||
labels: metadata_writer.Labels,
|
||||
score_calibration: Optional[metadata_writer.ScoreCalibration] = None
|
||||
) -> "MetadataWriter":
|
||||
"""Creates MetadataWriter to write the metadata for image classifier.
|
||||
|
||||
The parameters required in this method are mandatory when using MediaPipe
|
||||
Tasks.
|
||||
|
||||
Note that only the output TFLite is used for deployment. The output JSON
|
||||
content is used to interpret the metadata content.
|
||||
|
||||
Args:
|
||||
model_buffer: A valid flatbuffer loaded from the TFLite model file.
|
||||
input_norm_mean: the mean value used in the input tensor normalization
|
||||
[1].
|
||||
input_norm_std: the std value used in the input tensor normalizarion [1].
|
||||
labels: an instance of Labels helper class used in the output
|
||||
classification tensor [2].
|
||||
score_calibration: A container of the score calibration operation [3] in
|
||||
the classification tensor. Optional if the model does not use score
|
||||
calibration.
|
||||
|
||||
[1]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L389
|
||||
[2]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L99
|
||||
[3]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L456
|
||||
|
||||
Returns:
|
||||
A MetadataWriter object.
|
||||
"""
|
||||
writer = metadata_writer.MetadataWriter(model_buffer)
|
||||
writer.add_general_info(_MODEL_NAME, _MODEL_DESCRIPTION)
|
||||
writer.add_image_input(input_norm_mean, input_norm_std)
|
||||
writer.add_classification_output(labels, score_calibration)
|
||||
return cls(writer)
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Writes metadata and label file to the image segmenter models."""
|
||||
import enum
|
||||
from typing import List, Optional
|
||||
|
||||
import flatbuffers
|
||||
|
||||
from mediapipe.tasks.metadata import image_segmenter_metadata_schema_py_generated as _segmenter_metadata_fb
|
||||
from mediapipe.tasks.metadata import metadata_schema_py_generated as _metadata_fb
|
||||
from mediapipe.tasks.python.metadata import metadata
|
||||
from mediapipe.tasks.python.metadata.metadata_writers import metadata_info
|
||||
from mediapipe.tasks.python.metadata.metadata_writers import metadata_writer
|
||||
from mediapipe.tasks.python.metadata.metadata_writers import writer_utils
|
||||
|
||||
|
||||
_MODEL_NAME = "ImageSegmenter"
|
||||
_MODEL_DESCRIPTION = (
|
||||
"Semantic image segmentation predicts whether each pixel "
|
||||
"of an image is associated with a certain class."
|
||||
)
|
||||
|
||||
# Metadata Schema file for image segmenter.
|
||||
_FLATC_METADATA_SCHEMA_FILE = metadata.get_path_to_datafile(
|
||||
"../../../metadata/image_segmenter_metadata_schema.fbs",
|
||||
)
|
||||
|
||||
# Metadata name in custom metadata field. The metadata name is used to get
|
||||
# image segmenter metadata from SubGraphMetadata.custom_metadata and
|
||||
# shouldn't be changed.
|
||||
_METADATA_NAME = "SEGMENTER_METADATA"
|
||||
|
||||
|
||||
class Activation(enum.Enum):
|
||||
NONE = 0
|
||||
SIGMOID = 1
|
||||
SOFTMAX = 2
|
||||
|
||||
|
||||
# Create an individual method for getting the metadata json file, so that it can
|
||||
# be used as a standalone util.
|
||||
def convert_to_json(metadata_buffer: bytearray) -> str:
|
||||
"""Converts the metadata into a json string.
|
||||
|
||||
Args:
|
||||
metadata_buffer: valid metadata buffer in bytes.
|
||||
|
||||
Returns:
|
||||
Metadata in JSON format.
|
||||
|
||||
Raises:
|
||||
ValueError: error occurred when parsing the metadata schema file.
|
||||
"""
|
||||
return metadata.convert_to_json(
|
||||
metadata_buffer,
|
||||
custom_metadata_schema={_METADATA_NAME: _FLATC_METADATA_SCHEMA_FILE},
|
||||
)
|
||||
|
||||
|
||||
class ImageSegmenterOptionsMd(metadata_info.CustomMetadataMd):
|
||||
"""Image segmenter options metadata."""
|
||||
|
||||
_METADATA_FILE_IDENTIFIER = b"V001"
|
||||
|
||||
def __init__(self, activation: Activation) -> None:
|
||||
"""Creates an ImageSegmenterOptionsMd object.
|
||||
|
||||
Args:
|
||||
activation: activation function of the output layer in the image
|
||||
segmenter.
|
||||
"""
|
||||
self.activation = activation
|
||||
super().__init__(name=_METADATA_NAME)
|
||||
|
||||
def create_metadata(self) -> _metadata_fb.CustomMetadataT:
|
||||
"""Creates the image segmenter options metadata.
|
||||
|
||||
Returns:
|
||||
A Flatbuffers Python object of the custom metadata including image
|
||||
segmenter options metadata.
|
||||
"""
|
||||
segmenter_options = _segmenter_metadata_fb.ImageSegmenterOptionsT()
|
||||
segmenter_options.activation = self.activation.value
|
||||
|
||||
# Get the image segmenter options flatbuffer.
|
||||
b = flatbuffers.Builder(0)
|
||||
b.Finish(segmenter_options.Pack(b), self._METADATA_FILE_IDENTIFIER)
|
||||
segmenter_options_buf = b.Output()
|
||||
|
||||
# Add the image segmenter options flatbuffer in custom metadata.
|
||||
custom_metadata = _metadata_fb.CustomMetadataT()
|
||||
custom_metadata.name = self.name
|
||||
custom_metadata.data = segmenter_options_buf
|
||||
return custom_metadata
|
||||
|
||||
|
||||
class MetadataWriter(metadata_writer.MetadataWriterBase):
|
||||
"""MetadataWriter to write the metadata for image segmenter."""
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
model_buffer: bytearray,
|
||||
input_norm_mean: List[float],
|
||||
input_norm_std: List[float],
|
||||
labels: Optional[metadata_writer.Labels] = None,
|
||||
activation: Optional[Activation] = None,
|
||||
) -> "MetadataWriter":
|
||||
"""Creates MetadataWriter to write the metadata for image segmenter.
|
||||
|
||||
The parameters required in this method are mandatory when using MediaPipe
|
||||
Tasks.
|
||||
|
||||
Example usage:
|
||||
metadata_writer = image_segmenter.Metadatawriter.create(model_buffer, ...)
|
||||
tflite_content, json_content = metadata_writer.populate()
|
||||
|
||||
When calling `populate` function in this class, it returns TfLite content
|
||||
and JSON content. Note that only the output TFLite is used for deployment.
|
||||
The output JSON content is used to interpret the metadata content.
|
||||
|
||||
Args:
|
||||
model_buffer: A valid flatbuffer loaded from the TFLite model file.
|
||||
input_norm_mean: the mean value used in the input tensor normalization
|
||||
[1].
|
||||
input_norm_std: the std value used in the input tensor normalizarion [1].
|
||||
labels: an instance of Labels helper class used in the output category
|
||||
tensor [2].
|
||||
activation: activation function for the output layer.
|
||||
[1]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L389
|
||||
[2]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L116
|
||||
|
||||
Returns:
|
||||
A MetadataWriter object.
|
||||
"""
|
||||
writer = metadata_writer.MetadataWriter(model_buffer)
|
||||
writer.add_general_info(_MODEL_NAME, _MODEL_DESCRIPTION)
|
||||
writer.add_image_input(input_norm_mean, input_norm_std)
|
||||
if activation is not None:
|
||||
option_md = ImageSegmenterOptionsMd(activation)
|
||||
writer.add_custom_metadata(option_md)
|
||||
num_output_tensors = writer_utils.get_subgraph(model_buffer).OutputsLength()
|
||||
if num_output_tensors == 2:
|
||||
# For image segmenter model with 2 output tensors, the first one is
|
||||
# quality score, and the second one is matting mask.
|
||||
writer.add_feature_output(
|
||||
"quality score", "The quality score of matting result."
|
||||
)
|
||||
writer.add_segmentation_output(labels=labels)
|
||||
return cls(writer)
|
||||
|
||||
def populate(self) -> tuple[bytearray, str]:
|
||||
model_buf, _ = super().populate()
|
||||
metadata_buf = metadata.get_metadata_buffer(model_buf)
|
||||
json_content = convert_to_json(metadata_buf)
|
||||
return model_buf, json_content
|
||||
+1166
File diff suppressed because it is too large
Load Diff
+845
@@ -0,0 +1,845 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Generic metadata writer."""
|
||||
|
||||
import collections
|
||||
import csv
|
||||
import dataclasses
|
||||
import os
|
||||
import tempfile
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
import flatbuffers
|
||||
|
||||
from mediapipe.tasks.metadata import metadata_schema_py_generated as metadata_fb
|
||||
from mediapipe.tasks.python.metadata import metadata
|
||||
from mediapipe.tasks.python.metadata.metadata_writers import metadata_info
|
||||
from mediapipe.tasks.python.metadata.metadata_writers import writer_utils
|
||||
|
||||
_INPUT_IMAGE_NAME = 'image'
|
||||
_INPUT_IMAGE_DESCRIPTION = 'Input image to be processed.'
|
||||
_INPUT_REGEX_TEXT_NAME = 'input_text'
|
||||
_INPUT_REGEX_TEXT_DESCRIPTION = ('Embedding vectors representing the input '
|
||||
'text to be processed.')
|
||||
_OUTPUT_CLASSIFICATION_NAME = 'score'
|
||||
_OUTPUT_CLASSIFICATION_DESCRIPTION = 'Score of the labels respectively.'
|
||||
_OUTPUT_SEGMENTATION_MASKS_NAME = 'segmentation_masks'
|
||||
_OUTPUT_SEGMENTATION_MASKS_DESCRIPTION = (
|
||||
'Masks over the target objects with high accuracy.'
|
||||
)
|
||||
# Detection tensor result to be grouped together.
|
||||
_DETECTION_GROUP_NAME = 'detection_result'
|
||||
# File name to export score calibration parameters.
|
||||
_SCORE_CALIBATION_FILENAME = 'score_calibration.txt'
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class CalibrationParameter:
|
||||
"""Parameters for score calibration [1].
|
||||
|
||||
Score calibration is performed on an output tensor through sigmoid functions.
|
||||
One of the main purposes of score calibration is to make scores across classes
|
||||
comparable, so that a common threshold can be used for all output classes.
|
||||
|
||||
For each index in the output tensor, this applies:
|
||||
* `f(x) = scale / (1 + e^-(slope * g(x) + offset))` if `x > min_score` or if
|
||||
no `min_score` has been specified.
|
||||
* `f(x) = default_score` otherwise or if no scale, slope and offset have
|
||||
been specified.
|
||||
|
||||
[1]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L434
|
||||
"""
|
||||
scale: float
|
||||
slope: float
|
||||
offset: float
|
||||
min_score: Optional[float] = None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class LabelItem:
|
||||
"""Label item for labels per locale.
|
||||
|
||||
Attributes:
|
||||
filename: The file name to save the labels.
|
||||
names: A list of label names.
|
||||
locale: The specified locale for labels.
|
||||
"""
|
||||
filename: str
|
||||
names: List[str]
|
||||
locale: Optional[str] = None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ScoreThresholding:
|
||||
"""Parameters to performs thresholding on output tensor values [1].
|
||||
|
||||
Attributes:
|
||||
global_score_threshold: The recommended global threshold below which results
|
||||
are considered low-confidence and should be filtered out. [1]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L468
|
||||
"""
|
||||
global_score_threshold: float
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class RegexTokenizer:
|
||||
"""Parameters of the Regex tokenizer [1] metadata information.
|
||||
|
||||
[1]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L500
|
||||
|
||||
Attributes:
|
||||
delim_regex_pattern: the regular expression to segment strings and create
|
||||
tokens.
|
||||
vocab_file_path: path to the vocabulary file.
|
||||
"""
|
||||
delim_regex_pattern: str
|
||||
vocab_file_path: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class BertTokenizer:
|
||||
"""Parameters of the Bert tokenizer [1] metadata information.
|
||||
|
||||
[1]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L477
|
||||
|
||||
Attributes:
|
||||
vocab_file_path: path to the vocabulary file.
|
||||
"""
|
||||
vocab_file_path: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SentencePieceTokenizer:
|
||||
"""Parameters of the sentence piece tokenizer tokenizer [1] metadata information.
|
||||
|
||||
[1]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L485
|
||||
|
||||
Attributes:
|
||||
sentence_piece_model_path: path to the sentence piece model file.
|
||||
vocab_file_path: path to the vocabulary file.
|
||||
"""
|
||||
sentence_piece_model_path: str
|
||||
vocab_file_path: Optional[str] = None
|
||||
|
||||
|
||||
class Labels(object):
|
||||
"""Simple container holding classification labels of a particular tensor.
|
||||
|
||||
Example usage:
|
||||
# The first added label list can be used as category names as needed.
|
||||
labels = Labels()
|
||||
.add(['/m/011l78', '/m/031d23'])
|
||||
.add(['cat', 'dog], 'en')
|
||||
.add(['chat', 'chien], 'fr')
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._labels = [] # [LabelItem]
|
||||
|
||||
@property
|
||||
def labels(self) -> List[LabelItem]:
|
||||
return self._labels
|
||||
|
||||
def add(self,
|
||||
labels: List[str],
|
||||
locale: Optional[str] = None,
|
||||
exported_filename: Optional[str] = None) -> 'Labels':
|
||||
"""Adds labels in the container.
|
||||
|
||||
Args:
|
||||
labels: A list of label names, e.g. ['apple', 'pear', 'banana'].
|
||||
locale: The specified locale for labels.
|
||||
exported_filename: The file name to export the labels. If not set,
|
||||
filename defaults to 'labels.txt'.
|
||||
|
||||
Returns:
|
||||
The Labels instance, can be used for chained operation.
|
||||
"""
|
||||
if not labels:
|
||||
raise ValueError('The list of labels is empty.')
|
||||
|
||||
# Prepare the new item to be inserted
|
||||
if not exported_filename:
|
||||
exported_filename = 'labels'
|
||||
if locale:
|
||||
exported_filename += f'_{locale}'
|
||||
exported_filename += '.txt'
|
||||
item = LabelItem(filename=exported_filename, names=labels, locale=locale)
|
||||
|
||||
# Insert the new element at the end of the list
|
||||
self._labels.append(item)
|
||||
return self
|
||||
|
||||
def add_from_file(self,
|
||||
label_filepath: str,
|
||||
locale: Optional[str] = None,
|
||||
exported_filename: Optional[str] = None) -> 'Labels':
|
||||
"""Adds a label file in the container.
|
||||
|
||||
Args:
|
||||
label_filepath: File path to read labels. Each line is a label name in the
|
||||
file.
|
||||
locale: The specified locale for labels.
|
||||
exported_filename: The file name to export the labels. If not set,
|
||||
filename defaults to 'labels.txt'.
|
||||
|
||||
Returns:
|
||||
The Labels instance, can be used for chained operation.
|
||||
"""
|
||||
|
||||
with open(label_filepath, 'r') as f:
|
||||
labels = f.read().split('\n')
|
||||
return self.add(labels, locale, exported_filename)
|
||||
|
||||
|
||||
class ScoreCalibration:
|
||||
"""Simple container holding score calibration related parameters."""
|
||||
|
||||
# A shortcut to avoid client side code importing metadata_fb
|
||||
transformation_types = metadata_fb.ScoreTransformationType
|
||||
|
||||
def __init__(self,
|
||||
transformation_type: metadata_fb.ScoreTransformationType,
|
||||
parameters: List[Optional[CalibrationParameter]],
|
||||
default_score: int = 0):
|
||||
self.transformation_type = transformation_type
|
||||
self.parameters = parameters
|
||||
self.default_score = default_score
|
||||
|
||||
@classmethod
|
||||
def create_from_file(cls,
|
||||
transformation_type: metadata_fb.ScoreTransformationType,
|
||||
file_path: str,
|
||||
default_score: int = 0) -> 'ScoreCalibration':
|
||||
"""Creates ScoreCalibration from the file.
|
||||
|
||||
Args:
|
||||
transformation_type: type of the function used for transforming the
|
||||
uncalibrated score before applying score calibration.
|
||||
file_path: file_path of the score calibration file [1]. Contains
|
||||
sigmoid-based score calibration parameters, formatted as CSV. Lines
|
||||
contain for each index of an output tensor the scale, slope, offset and
|
||||
(optional) min_score parameters to be used for sigmoid fitting (in this
|
||||
order and in `strtof`-compatible [2] format). Scale should be a
|
||||
non-negative value. A line may be left empty to default calibrated
|
||||
scores for this index to default_score. In summary, each line should
|
||||
thus contain 0, 3 or 4 comma-separated values.
|
||||
default_score: the default calibrated score to apply if the uncalibrated
|
||||
score is below min_score or if no parameters were specified for a given
|
||||
index.
|
||||
[1]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L133
|
||||
[2]:
|
||||
https://en.cppreference.com/w/c/string/byte/strtof
|
||||
|
||||
Returns:
|
||||
A ScoreCalibration object.
|
||||
Raises:
|
||||
ValueError: if the score_calibration file is malformed.
|
||||
"""
|
||||
with open(file_path, 'r') as calibration_file:
|
||||
csv_reader = csv.reader(calibration_file, delimiter=',')
|
||||
parameters = []
|
||||
for row in csv_reader:
|
||||
if not row:
|
||||
parameters.append(None)
|
||||
continue
|
||||
|
||||
if len(row) != 3 and len(row) != 4:
|
||||
raise ValueError(
|
||||
f'Expected empty lines or 3 or 4 parameters per line in score'
|
||||
f' calibration file, but got {len(row)}.')
|
||||
|
||||
if float(row[0]) < 0:
|
||||
raise ValueError(
|
||||
f'Expected scale to be a non-negative value, but got '
|
||||
f'{float(row[0])}.')
|
||||
|
||||
parameters.append(
|
||||
CalibrationParameter(
|
||||
scale=float(row[0]),
|
||||
slope=float(row[1]),
|
||||
offset=float(row[2]),
|
||||
min_score=None if len(row) == 3 else float(row[3])))
|
||||
|
||||
return cls(transformation_type, parameters, default_score)
|
||||
|
||||
|
||||
def _fill_default_tensor_names(
|
||||
tensor_metadata_list: List[metadata_fb.TensorMetadataT],
|
||||
tensor_names_from_model: List[str]):
|
||||
"""Fills the default tensor names."""
|
||||
# If tensor name in metadata is empty, default to the tensor name saved in
|
||||
# the model.
|
||||
for tensor_metadata, name in zip(tensor_metadata_list,
|
||||
tensor_names_from_model):
|
||||
tensor_metadata.name = tensor_metadata.name or name
|
||||
|
||||
|
||||
def _pair_tensor_metadata(
|
||||
tensor_md: List[metadata_info.TensorMd],
|
||||
tensor_names_from_model: List[str]) -> List[metadata_info.TensorMd]:
|
||||
"""Pairs tensor_md according to the tensor names from the model."""
|
||||
tensor_names_from_arg = [
|
||||
md.tensor_name for md in tensor_md or [] if md.tensor_name is not None
|
||||
]
|
||||
if not tensor_names_from_arg:
|
||||
return tensor_md
|
||||
|
||||
if collections.Counter(tensor_names_from_arg) != collections.Counter(
|
||||
tensor_names_from_model):
|
||||
raise ValueError(
|
||||
'The tensor names from arguments ({}) do not match the tensor names'
|
||||
' read from the model ({}).'.format(tensor_names_from_arg,
|
||||
tensor_names_from_model))
|
||||
pairs_tensor_md = []
|
||||
name_md_dict = dict(zip(tensor_names_from_arg, tensor_md))
|
||||
for name in tensor_names_from_model:
|
||||
pairs_tensor_md.append(name_md_dict[name])
|
||||
return pairs_tensor_md
|
||||
|
||||
|
||||
def _create_metadata_buffer(
|
||||
model_buffer: bytearray,
|
||||
general_md: Optional[metadata_info.GeneralMd] = None,
|
||||
input_md: Optional[List[metadata_info.TensorMd]] = None,
|
||||
output_md: Optional[List[metadata_info.TensorMd]] = None,
|
||||
input_process_units: Optional[List[metadata_fb.ProcessUnitT]] = None,
|
||||
output_group_md: Optional[List[metadata_info.TensorGroupMd]] = None,
|
||||
custom_metadata_md: Optional[List[metadata_info.CustomMetadataMd]] = None,
|
||||
) -> bytearray:
|
||||
"""Creates a buffer of the metadata.
|
||||
|
||||
Args:
|
||||
model_buffer: valid buffer of the model file.
|
||||
general_md: general information about the model.
|
||||
input_md: metadata information of the input tensors.
|
||||
output_md: metadata information of the output tensors.
|
||||
input_process_units: a lists of metadata of the input process units [1].
|
||||
output_group_md: a list of metadata of output tensor groups [2];
|
||||
custom_metadata_md: a lists of custom metadata.
|
||||
[1]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L655
|
||||
[2]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L677
|
||||
|
||||
Returns:
|
||||
A buffer of the metadata.
|
||||
|
||||
Raises:
|
||||
ValueError: if the tensor names from `input_md` and `output_md` do not
|
||||
match the tensor names read from the model.
|
||||
"""
|
||||
# Create input metadata from `input_md`.
|
||||
if input_md:
|
||||
input_md = _pair_tensor_metadata(
|
||||
input_md, writer_utils.get_input_tensor_names(model_buffer))
|
||||
input_metadata = [m.create_metadata() for m in input_md]
|
||||
else:
|
||||
num_input_tensors = writer_utils.get_subgraph(model_buffer).InputsLength()
|
||||
input_metadata = [metadata_fb.TensorMetadataT()] * num_input_tensors
|
||||
|
||||
_fill_default_tensor_names(input_metadata,
|
||||
writer_utils.get_input_tensor_names(model_buffer))
|
||||
|
||||
# Create output metadata from `output_md`.
|
||||
if output_md:
|
||||
output_md = _pair_tensor_metadata(
|
||||
output_md, writer_utils.get_output_tensor_names(model_buffer))
|
||||
output_metadata = [m.create_metadata() for m in output_md]
|
||||
else:
|
||||
num_output_tensors = writer_utils.get_subgraph(model_buffer).OutputsLength()
|
||||
output_metadata = [metadata_fb.TensorMetadataT()] * num_output_tensors
|
||||
_fill_default_tensor_names(output_metadata,
|
||||
writer_utils.get_output_tensor_names(model_buffer))
|
||||
|
||||
# Create the subgraph metadata.
|
||||
subgraph_metadata = metadata_fb.SubGraphMetadataT()
|
||||
subgraph_metadata.inputTensorMetadata = input_metadata
|
||||
subgraph_metadata.outputTensorMetadata = output_metadata
|
||||
if input_process_units:
|
||||
subgraph_metadata.inputProcessUnits = input_process_units
|
||||
if custom_metadata_md:
|
||||
subgraph_metadata.customMetadata = [
|
||||
m.create_metadata() for m in custom_metadata_md
|
||||
]
|
||||
if output_group_md:
|
||||
subgraph_metadata.outputTensorGroups = [
|
||||
m.create_metadata() for m in output_group_md
|
||||
]
|
||||
|
||||
# Create the whole model metadata.
|
||||
if general_md is None:
|
||||
general_md = metadata_info.GeneralMd()
|
||||
model_metadata = general_md.create_metadata()
|
||||
model_metadata.subgraphMetadata = [subgraph_metadata]
|
||||
|
||||
# Get the metadata flatbuffer.
|
||||
b = flatbuffers.Builder(0)
|
||||
b.Finish(
|
||||
model_metadata.Pack(b),
|
||||
metadata.MetadataPopulator.METADATA_FILE_IDENTIFIER)
|
||||
return b.Output()
|
||||
|
||||
|
||||
class MetadataWriter(object):
|
||||
"""Generic Metadata writer.
|
||||
|
||||
Example usage:
|
||||
|
||||
For an example model which requires two inputs: image and general feature
|
||||
inputs, and generates one output: classification.
|
||||
|
||||
with open(model_path, 'rb') as f:
|
||||
writer = MetadataWriter.create(f.read())
|
||||
model_content, metadata_json_content = writer
|
||||
.add_genernal_info('model_name', 'model description')
|
||||
.add_image_input()
|
||||
.add_feature_input()
|
||||
.add_classification_output(Labels().add(['A', 'B']))
|
||||
.populate()
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def create(cls, model_buffer: bytearray) -> 'MetadataWriter':
|
||||
return cls(model_buffer)
|
||||
|
||||
def __init__(self, model_buffer: bytearray) -> None:
|
||||
self._model_buffer = model_buffer
|
||||
self._general_md = None
|
||||
self._input_mds = []
|
||||
self._input_process_units = []
|
||||
self._output_mds = []
|
||||
self._output_group_mds = []
|
||||
self._associated_files = []
|
||||
self._custom_metadata_mds = []
|
||||
self._temp_folder = tempfile.TemporaryDirectory()
|
||||
|
||||
def __del__(self):
|
||||
if os.path.exists(self._temp_folder.name):
|
||||
self._temp_folder.cleanup()
|
||||
|
||||
def add_general_info(
|
||||
self,
|
||||
model_name: str,
|
||||
model_description: Optional[str] = None) -> 'MetadataWriter':
|
||||
"""Adds a general info metadata for the general metadata informantion."""
|
||||
# Will overwrite the previous `self._general_md` if exists.
|
||||
self._general_md = metadata_info.GeneralMd(
|
||||
name=model_name, description=model_description)
|
||||
return self
|
||||
|
||||
color_space_types = metadata_fb.ColorSpaceType
|
||||
|
||||
def add_feature_input(self,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None) -> 'MetadataWriter':
|
||||
"""Adds an input tensor metadata for the general basic feature input."""
|
||||
input_md = metadata_info.TensorMd(name=name, description=description)
|
||||
self._input_mds.append(input_md)
|
||||
return self
|
||||
|
||||
def add_image_input(
|
||||
self,
|
||||
norm_mean: List[float],
|
||||
norm_std: List[float],
|
||||
color_space_type: Optional[int] = metadata_fb.ColorSpaceType.RGB,
|
||||
name: str = _INPUT_IMAGE_NAME,
|
||||
description: str = _INPUT_IMAGE_DESCRIPTION) -> 'MetadataWriter':
|
||||
"""Adds an input image metadata for the image input.
|
||||
|
||||
Args:
|
||||
norm_mean: The mean value used to normalize each input channel. If there
|
||||
is only one element in the list, its value will be broadcasted to all
|
||||
channels. Also note that norm_mean and norm_std should have the same
|
||||
number of elements. [1]
|
||||
norm_std: The std value used to normalize each input channel. If there is
|
||||
only one element in the list, its value will be broadcasted to all
|
||||
channels. [1]
|
||||
color_space_type: The color space type of the input image. [2]
|
||||
name: Name of the input tensor.
|
||||
description: Description of the input tensor.
|
||||
|
||||
Returns:
|
||||
The MetadataWriter instance, can be used for chained operation.
|
||||
|
||||
[1]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L389
|
||||
[2]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L198
|
||||
"""
|
||||
input_md = metadata_info.InputImageTensorMd(
|
||||
name=name,
|
||||
description=description,
|
||||
norm_mean=norm_mean,
|
||||
norm_std=norm_std,
|
||||
color_space_type=color_space_type,
|
||||
tensor_type=self._input_tensor_type(len(self._input_mds)))
|
||||
|
||||
self._input_mds.append(input_md)
|
||||
return self
|
||||
|
||||
def add_regex_text_input(
|
||||
self,
|
||||
regex_tokenizer: RegexTokenizer,
|
||||
name: str = _INPUT_REGEX_TEXT_NAME,
|
||||
description: str = _INPUT_REGEX_TEXT_DESCRIPTION) -> 'MetadataWriter':
|
||||
"""Adds an input text metadata for the text input with regex tokenizer.
|
||||
|
||||
Args:
|
||||
regex_tokenizer: information of the regex tokenizer [1] used to process
|
||||
the input string.
|
||||
name: Name of the input tensor.
|
||||
description: Description of the input tensor.
|
||||
|
||||
Returns:
|
||||
The MetadataWriter instance, can be used for chained operation.
|
||||
|
||||
[1]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L500
|
||||
"""
|
||||
tokenizer_md = metadata_info.RegexTokenizerMd(
|
||||
delim_regex_pattern=regex_tokenizer.delim_regex_pattern,
|
||||
vocab_file_path=regex_tokenizer.vocab_file_path)
|
||||
input_md = metadata_info.InputTextTensorMd(
|
||||
name=name, description=description, tokenizer_md=tokenizer_md)
|
||||
self._input_mds.append(input_md)
|
||||
self._associated_files.append(regex_tokenizer.vocab_file_path)
|
||||
return self
|
||||
|
||||
def add_bert_text_input(self, tokenizer: Union[BertTokenizer,
|
||||
SentencePieceTokenizer],
|
||||
ids_name: str, mask_name: str,
|
||||
segment_name: str) -> 'MetadataWriter':
|
||||
"""Adds an metadata for the text input with bert / sentencepiece tokenizer.
|
||||
|
||||
`ids_name`, `mask_name`, and `segment_name` correspond to the `Tensor.name`
|
||||
in the TFLite schema, which help to determine the tensor order when
|
||||
populating metadata.
|
||||
|
||||
Args:
|
||||
tokenizer: information of the tokenizer used to process the input string,
|
||||
if any. Supported tokenziers are: `BertTokenizer` [1] and
|
||||
`SentencePieceTokenizer` [2].
|
||||
ids_name: name of the ids tensor, which represents the tokenized ids of
|
||||
the input text.
|
||||
mask_name: name of the mask tensor, which represents the mask with `1` for
|
||||
real tokens and `0` for padding tokens.
|
||||
segment_name: name of the segment ids tensor, where `0` stands for the
|
||||
first sequence, and `1` stands for the second sequence if exists.
|
||||
|
||||
Returns:
|
||||
The MetadataWriter instance, can be used for chained operation.
|
||||
|
||||
Raises:
|
||||
ValueError: if the type tokenizer is not BertTokenizer or
|
||||
SentencePieceTokenizer.
|
||||
|
||||
[1]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L477
|
||||
[2]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L485
|
||||
"""
|
||||
if isinstance(tokenizer, BertTokenizer):
|
||||
tokenizer_md = metadata_info.BertTokenizerMd(
|
||||
vocab_file_path=tokenizer.vocab_file_path)
|
||||
elif isinstance(tokenizer, SentencePieceTokenizer):
|
||||
tokenizer_md = metadata_info.SentencePieceTokenizerMd(
|
||||
sentence_piece_model_path=tokenizer.sentence_piece_model_path,
|
||||
vocab_file_path=tokenizer.vocab_file_path)
|
||||
else:
|
||||
raise ValueError(
|
||||
f'The type of tokenizer, {type(tokenizer)}, is unsupported')
|
||||
bert_input_md = metadata_info.BertInputTensorsMd(
|
||||
self._model_buffer,
|
||||
ids_name,
|
||||
mask_name,
|
||||
segment_name,
|
||||
tokenizer_md=tokenizer_md)
|
||||
|
||||
self._input_mds.extend(bert_input_md.input_md)
|
||||
self._associated_files.extend(
|
||||
bert_input_md.get_tokenizer_associated_files())
|
||||
self._input_process_units.extend(
|
||||
bert_input_md.create_input_process_unit_metadata())
|
||||
return self
|
||||
|
||||
def add_classification_output(
|
||||
self,
|
||||
labels: Optional[Labels] = None,
|
||||
score_calibration: Optional[ScoreCalibration] = None,
|
||||
score_thresholding: Optional[ScoreThresholding] = None,
|
||||
name: str = _OUTPUT_CLASSIFICATION_NAME,
|
||||
description: str = _OUTPUT_CLASSIFICATION_DESCRIPTION
|
||||
) -> 'MetadataWriter':
|
||||
"""Add a classification head metadata for classification output tensor.
|
||||
|
||||
Example usage:
|
||||
writer.add_classification_output(
|
||||
Labels()
|
||||
.add(['/m/011l78', '/m/031d23'])
|
||||
.add(['cat', 'dog], 'en')
|
||||
.add(['chat', 'chien], 'fr')
|
||||
)
|
||||
|
||||
Args:
|
||||
labels: an instance of Labels helper class.
|
||||
score_calibration: an instance of ScoreCalibration helper class.
|
||||
score_thresholding: an instance of ScoreThresholding.
|
||||
name: Metadata name of the tensor. Note that this is different from tensor
|
||||
name in the flatbuffer.
|
||||
description: human readable description of what the output is.
|
||||
|
||||
Returns:
|
||||
The current Writer instance to allow chained operation.
|
||||
"""
|
||||
calibration_md = self._create_score_calibration_md(score_calibration)
|
||||
score_thresholding_md = None
|
||||
if score_thresholding:
|
||||
score_thresholding_md = metadata_info.ScoreThresholdingMd(
|
||||
score_thresholding.global_score_threshold)
|
||||
|
||||
label_files = self._create_label_file_md(labels)
|
||||
output_md = metadata_info.ClassificationTensorMd(
|
||||
name=name,
|
||||
description=description,
|
||||
label_files=label_files,
|
||||
tensor_type=self._output_tensor_type(len(self._output_mds)),
|
||||
score_calibration_md=calibration_md,
|
||||
score_thresholding_md=score_thresholding_md,
|
||||
)
|
||||
self._output_mds.append(output_md)
|
||||
return self
|
||||
|
||||
def add_detection_output(
|
||||
self,
|
||||
labels: Optional[Labels] = None,
|
||||
score_calibration: Optional[ScoreCalibration] = None,
|
||||
group_name: str = _DETECTION_GROUP_NAME,
|
||||
) -> 'MetadataWriter':
|
||||
"""Adds a detection head metadata for detection output tensor of models with postprocessing.
|
||||
|
||||
Args:
|
||||
labels: an instance of Labels helper class.
|
||||
score_calibration: an instance of ScoreCalibration helper class.
|
||||
group_name: name of output tensor group.
|
||||
|
||||
Returns:
|
||||
The current Writer instance to allow chained operation.
|
||||
"""
|
||||
calibration_md = self._create_score_calibration_md(score_calibration)
|
||||
label_files = self._create_label_file_md(labels)
|
||||
detection_output_mds = metadata_info.DetectionOutputTensorsMd(
|
||||
self._model_buffer,
|
||||
label_files=label_files,
|
||||
score_calibration_md=calibration_md,
|
||||
).output_mds
|
||||
self._output_mds.extend(detection_output_mds)
|
||||
# Outputs are location, category, score, number of detections.
|
||||
if len(detection_output_mds) != 4:
|
||||
raise ValueError('The size of detections output should be 4.')
|
||||
# The first 3 tensors (location, category, score) are grouped.
|
||||
group_md = metadata_info.TensorGroupMd(
|
||||
name=group_name,
|
||||
tensor_names=[output_md.name for output_md in detection_output_mds[:3]],
|
||||
)
|
||||
self._output_group_mds.append(group_md)
|
||||
return self
|
||||
|
||||
def add_raw_detection_output(
|
||||
self,
|
||||
labels: Optional[Labels] = None,
|
||||
output_tensors_order: metadata_info.RawDetectionOutputTensorsOrder = metadata_info.RawDetectionOutputTensorsOrder.UNSPECIFIED,
|
||||
) -> 'MetadataWriter':
|
||||
"""Adds a detection head metadata for detection output tensor of models without postprocessing.
|
||||
|
||||
Args:
|
||||
labels: an instance of Labels helper class.
|
||||
output_tensors_order: the order of the output tensors. For models of
|
||||
out-of-graph non-maximum-suppression only.
|
||||
|
||||
Returns:
|
||||
The current Writer instance to allow chained operation.
|
||||
"""
|
||||
label_files = self._create_label_file_md(labels)
|
||||
detection_output_mds = metadata_info.RawDetectionOutputTensorsMd(
|
||||
self._model_buffer,
|
||||
label_files=label_files,
|
||||
output_tensors_order=output_tensors_order,
|
||||
).output_mds
|
||||
self._output_mds.extend(detection_output_mds)
|
||||
# Outputs are location, score.
|
||||
if len(detection_output_mds) != 2:
|
||||
raise ValueError('The size of detections output should be 2.')
|
||||
return self
|
||||
|
||||
def add_segmentation_output(
|
||||
self,
|
||||
labels: Optional[Labels] = None,
|
||||
name: str = _OUTPUT_SEGMENTATION_MASKS_NAME,
|
||||
description: str = _OUTPUT_SEGMENTATION_MASKS_DESCRIPTION,
|
||||
) -> 'MetadataWriter':
|
||||
"""Adds a segmentation head metadata for segmentation output tensor.
|
||||
|
||||
Args:
|
||||
labels: an instance of Labels helper class.
|
||||
name: Metadata name of the tensor. Note that this is different from tensor
|
||||
name in the flatbuffer.
|
||||
description: human readable description of what the output is.
|
||||
|
||||
Returns:
|
||||
The current Writer instance to allow chained operation.
|
||||
"""
|
||||
label_files = self._create_label_file_md(labels)
|
||||
output_md = metadata_info.SegmentationMaskMd(
|
||||
name=name,
|
||||
description=description,
|
||||
label_files=label_files,
|
||||
)
|
||||
self._output_mds.append(output_md)
|
||||
return self
|
||||
|
||||
def add_feature_output(self,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None) -> 'MetadataWriter':
|
||||
"""Adds an output tensor metadata for the general basic feature output."""
|
||||
output_md = metadata_info.TensorMd(name=name, description=description)
|
||||
self._output_mds.append(output_md)
|
||||
return self
|
||||
|
||||
def add_custom_metadata(
|
||||
self, custom_metadata_md: metadata_info.CustomMetadataMd
|
||||
) -> 'MetadataWriter':
|
||||
self._custom_metadata_mds.append(custom_metadata_md)
|
||||
return self
|
||||
|
||||
def populate(self) -> Tuple[bytearray, str]:
|
||||
"""Populates metadata into the TFLite file.
|
||||
|
||||
Note that only the output tflite is used for deployment. The output JSON
|
||||
content is used to interpret the metadata content.
|
||||
|
||||
Returns:
|
||||
A tuple of (model_with_metadata_in_bytes, metadata_json_content)
|
||||
"""
|
||||
# Populates metadata and associated files into TFLite model buffer.
|
||||
populator = metadata.MetadataPopulator.with_model_buffer(self._model_buffer)
|
||||
metadata_buffer = _create_metadata_buffer(
|
||||
model_buffer=self._model_buffer,
|
||||
general_md=self._general_md,
|
||||
input_md=self._input_mds,
|
||||
output_md=self._output_mds,
|
||||
input_process_units=self._input_process_units,
|
||||
custom_metadata_md=self._custom_metadata_mds,
|
||||
output_group_md=self._output_group_mds,
|
||||
)
|
||||
populator.load_metadata_buffer(metadata_buffer)
|
||||
if self._associated_files:
|
||||
populator.load_associated_files(self._associated_files)
|
||||
populator.populate()
|
||||
tflite_content = populator.get_model_buffer()
|
||||
|
||||
displayer = metadata.MetadataDisplayer.with_model_buffer(tflite_content)
|
||||
metadata_json_content = displayer.get_metadata_json()
|
||||
|
||||
return tflite_content, metadata_json_content
|
||||
|
||||
def _input_tensor_type(self, idx):
|
||||
return writer_utils.get_input_tensor_types(self._model_buffer)[idx]
|
||||
|
||||
def _output_tensor_type(self, idx):
|
||||
return writer_utils.get_output_tensor_types(self._model_buffer)[idx]
|
||||
|
||||
def _export_labels(self, filename: str, index_to_label: List[str]) -> str:
|
||||
filepath = os.path.join(self._temp_folder.name, filename)
|
||||
with open(filepath, 'w') as f:
|
||||
f.write('\n'.join(index_to_label))
|
||||
self._associated_files.append(filepath)
|
||||
return filepath
|
||||
|
||||
def _export_calibration_file(self, filename: str,
|
||||
calibrations: List[CalibrationParameter]) -> str:
|
||||
"""Stores calibration parameters in a csv file."""
|
||||
filepath = os.path.join(self._temp_folder.name, filename)
|
||||
with open(filepath, 'w') as f:
|
||||
for item in calibrations:
|
||||
if item:
|
||||
if item.scale is None or item.slope is None or item.offset is None:
|
||||
raise ValueError('scale, slope and offset values can not be set to '
|
||||
'None.')
|
||||
elif item.min_score is not None:
|
||||
f.write(f'{item.scale},{item.slope},{item.offset},{item.min_score}')
|
||||
else:
|
||||
f.write(f'{item.scale},{item.slope},{item.offset}')
|
||||
f.write('\n')
|
||||
|
||||
self._associated_files.append(filepath)
|
||||
return filepath
|
||||
|
||||
def _create_score_calibration_md(
|
||||
self, score_calibration: ScoreCalibration
|
||||
) -> Optional[metadata_info.ScoreCalibrationMd]:
|
||||
"""Creates the ScoreCalibrationMd object."""
|
||||
if score_calibration is None:
|
||||
return None
|
||||
return metadata_info.ScoreCalibrationMd(
|
||||
score_transformation_type=score_calibration.transformation_type,
|
||||
default_score=score_calibration.default_score,
|
||||
file_path=self._export_calibration_file(
|
||||
_SCORE_CALIBATION_FILENAME, score_calibration.parameters
|
||||
),
|
||||
)
|
||||
|
||||
def _create_label_file_md(
|
||||
self, labels: Optional[Labels] = None
|
||||
) -> Optional[List[metadata_info.LabelFileMd]]:
|
||||
"""Creates a list of LabelFileMd objects."""
|
||||
label_files = None
|
||||
if labels:
|
||||
label_files = []
|
||||
for item in labels.labels:
|
||||
label_files.append(
|
||||
metadata_info.LabelFileMd(
|
||||
self._export_labels(item.filename, item.names),
|
||||
locale=item.locale,
|
||||
)
|
||||
)
|
||||
return label_files
|
||||
|
||||
|
||||
class MetadataWriterBase:
|
||||
"""Base MetadataWriter class which contains the apis exposed to users.
|
||||
|
||||
MetadataWriter for Tasks e.g. image classifier / object detector will inherit
|
||||
this class for their own usage.
|
||||
"""
|
||||
|
||||
def __init__(self, writer: MetadataWriter) -> None:
|
||||
self.writer = writer
|
||||
|
||||
def populate(self) -> Tuple[bytearray, str]:
|
||||
"""Populates metadata into the TFLite file.
|
||||
|
||||
Note that only the output tflite is used for deployment. The output JSON
|
||||
content is used to interpret the metadata content.
|
||||
|
||||
Returns:
|
||||
A tuple of (model_with_metadata_in_bytes, metadata_json_content)
|
||||
"""
|
||||
return self.writer.populate()
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
# Copyright 2023 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Utility methods for creating the model asset bundles."""
|
||||
|
||||
from typing import Dict
|
||||
import zipfile
|
||||
|
||||
# Alignment that ensures that all uncompressed files in the model bundle file
|
||||
# are aligned relative to the start of the file. This lets the files be
|
||||
# accessed directly via mmap.
|
||||
_ALIGNMENT = 4
|
||||
|
||||
|
||||
class AlignZipFile(zipfile.ZipFile):
|
||||
"""ZipFile that stores uncompressed files at particular alignment."""
|
||||
|
||||
def __init__(self, *args, alignment: int, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
assert alignment > 0
|
||||
self._alignment = alignment
|
||||
|
||||
def _writecheck(self, zinfo: zipfile.ZipInfo) -> None:
|
||||
# Aligned the uncompressed files.
|
||||
if zinfo.compress_type == zipfile.ZIP_STORED:
|
||||
offset = self.fp.tell()
|
||||
header_length = len(zinfo.FileHeader())
|
||||
padding_length = (
|
||||
self._alignment - (offset + header_length) % self._alignment
|
||||
)
|
||||
if padding_length:
|
||||
offset += padding_length
|
||||
self.fp.write(b"\x00" * padding_length)
|
||||
assert self.fp.tell() == offset
|
||||
zinfo.header_offset = offset
|
||||
else:
|
||||
raise ValueError(
|
||||
"Only support the uncompressed file (compress_type =="
|
||||
" zipfile.ZIP_STORED) in zip. The current file compress type is "
|
||||
+ str(zinfo.compress_type)
|
||||
)
|
||||
super()._writecheck(zinfo)
|
||||
|
||||
|
||||
def create_model_asset_bundle(
|
||||
input_models: Dict[str, bytes], output_path: str
|
||||
) -> None:
|
||||
"""Creates the model asset bundle.
|
||||
|
||||
Args:
|
||||
input_models: A dict of input models with key as the model file name and
|
||||
value as the model content.
|
||||
output_path: The output file path to save the model asset bundle.
|
||||
"""
|
||||
if not input_models or len(input_models) < 2:
|
||||
raise ValueError("Needs at least two input models for model asset bundle.")
|
||||
|
||||
with AlignZipFile(output_path, mode="w", alignment=_ALIGNMENT) as zf:
|
||||
for file_name, file_buffer in input_models.items():
|
||||
zf.writestr(file_name, file_buffer)
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
# Copyright 2023 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Writes metadata and label file to the Object Detector models."""
|
||||
|
||||
import dataclasses
|
||||
from typing import List, Optional
|
||||
|
||||
import flatbuffers
|
||||
|
||||
from mediapipe.tasks.metadata import metadata_schema_py_generated as _metadata_fb
|
||||
from mediapipe.tasks.metadata import object_detector_metadata_schema_py_generated as _detector_metadata_fb
|
||||
from mediapipe.tasks.python.metadata import metadata
|
||||
from mediapipe.tasks.python.metadata.metadata_writers import metadata_info
|
||||
from mediapipe.tasks.python.metadata.metadata_writers import metadata_writer
|
||||
|
||||
_MODEL_NAME = "ObjectDetector"
|
||||
_MODEL_DESCRIPTION = (
|
||||
"Identify which of a known set of objects might be present and provide "
|
||||
"information about their positions within the given image or a video "
|
||||
"stream."
|
||||
)
|
||||
|
||||
# Metadata Schema file for object detector.
|
||||
_FLATC_METADATA_SCHEMA_FILE = metadata.get_path_to_datafile(
|
||||
"../../../metadata/object_detector_metadata_schema.fbs",
|
||||
)
|
||||
|
||||
# Metadata name in custom metadata field. The metadata name is used to get
|
||||
# object detector metadata from SubGraphMetadata.custom_metadata and shouldn't
|
||||
# be changed.
|
||||
_METADATA_NAME = "DETECTOR_METADATA"
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class FixedAnchor:
|
||||
"""A fixed size anchor."""
|
||||
|
||||
x_center: float
|
||||
y_center: float
|
||||
width: Optional[float]
|
||||
height: Optional[float]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class FixedAnchorsSchema:
|
||||
"""The schema for a list of anchors with fixed size."""
|
||||
|
||||
anchors: List[FixedAnchor]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SsdAnchorsOptions:
|
||||
"""The ssd anchors options used in object detector model."""
|
||||
|
||||
fixed_anchors_schema: Optional[FixedAnchorsSchema]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TensorsDecodingOptions:
|
||||
"""The decoding options to convert model output tensors to detections."""
|
||||
|
||||
# The number of output classes predicted by the detection model.
|
||||
num_classes: int
|
||||
# The number of output boxes predicted by the detection model.
|
||||
num_boxes: int
|
||||
# The number of output values per boxes predicted by the detection
|
||||
# model. The values contain bounding boxes, keypoints, etc.
|
||||
num_coords: int
|
||||
# The offset of keypoint coordinates in the location tensor.
|
||||
keypoint_coord_offset: int
|
||||
# The number of predicted keypoints.
|
||||
num_keypoints: int
|
||||
# The dimension of each keypoint, e.g. number of values predicted for each
|
||||
# keypoint.
|
||||
num_values_per_keypoint: int
|
||||
# Parameters for decoding SSD detection model.
|
||||
x_scale: float
|
||||
y_scale: float
|
||||
w_scale: float
|
||||
h_scale: float
|
||||
# Whether to apply exponential on box size.
|
||||
apply_exponential_on_box_size: bool
|
||||
# Whether to apply sigmod function on the score.
|
||||
sigmoid_score: bool
|
||||
|
||||
|
||||
# Create an individual method for getting the metadata json file, so that it can
|
||||
# be used as a standalone util.
|
||||
def convert_to_json(metadata_buffer: bytearray) -> str:
|
||||
"""Converts the metadata into a json string.
|
||||
|
||||
Args:
|
||||
metadata_buffer: valid metadata buffer in bytes.
|
||||
|
||||
Returns:
|
||||
Metadata in JSON format.
|
||||
|
||||
Raises:
|
||||
ValueError: error occurred when parsing the metadata schema file.
|
||||
"""
|
||||
return metadata.convert_to_json(
|
||||
metadata_buffer,
|
||||
custom_metadata_schema={_METADATA_NAME: _FLATC_METADATA_SCHEMA_FILE},
|
||||
)
|
||||
|
||||
|
||||
class ObjectDetectorOptionsMd(metadata_info.CustomMetadataMd):
|
||||
"""Object detector options metadata."""
|
||||
|
||||
_METADATA_FILE_IDENTIFIER = b"V001"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ssd_anchors_options: SsdAnchorsOptions,
|
||||
tensors_decoding_options: TensorsDecodingOptions,
|
||||
) -> None:
|
||||
"""Creates an ObjectDetectorOptionsMd object.
|
||||
|
||||
Args:
|
||||
ssd_anchors_options: the ssd anchors options associated to the object
|
||||
detector model.
|
||||
tensors_decoding_options: the tensors decoding options used to decode the
|
||||
object detector model output.
|
||||
"""
|
||||
if ssd_anchors_options.fixed_anchors_schema is None:
|
||||
raise ValueError(
|
||||
"Currently only support FixedAnchorsSchema, which cannot be found"
|
||||
" in ssd_anchors_options."
|
||||
)
|
||||
self.ssd_anchors_options = ssd_anchors_options
|
||||
self.tensors_decoding_options = tensors_decoding_options
|
||||
super().__init__(name=_METADATA_NAME)
|
||||
|
||||
def create_metadata(self) -> _metadata_fb.CustomMetadataT:
|
||||
"""Creates the image segmenter options metadata.
|
||||
|
||||
Returns:
|
||||
A Flatbuffers Python object of the custom metadata including object
|
||||
detector options metadata.
|
||||
"""
|
||||
detector_options = _detector_metadata_fb.ObjectDetectorOptionsT()
|
||||
|
||||
# Set ssd_anchors_options.
|
||||
ssd_anchors_options = _detector_metadata_fb.SsdAnchorsOptionsT()
|
||||
fixed_anchors_schema = _detector_metadata_fb.FixedAnchorsSchemaT()
|
||||
fixed_anchors_schema.anchors = []
|
||||
for anchor in self.ssd_anchors_options.fixed_anchors_schema.anchors:
|
||||
anchor_t = _detector_metadata_fb.FixedAnchorT()
|
||||
anchor_t.xCenter = anchor.x_center
|
||||
anchor_t.yCenter = anchor.y_center
|
||||
anchor_t.width = anchor.width
|
||||
anchor_t.height = anchor.height
|
||||
fixed_anchors_schema.anchors.append(anchor_t)
|
||||
ssd_anchors_options.fixedAnchorsSchema = fixed_anchors_schema
|
||||
detector_options.ssdAnchorsOptions = ssd_anchors_options
|
||||
|
||||
# Set tensors_decoding_options.
|
||||
tensors_decoding_options = _detector_metadata_fb.TensorsDecodingOptionsT()
|
||||
tensors_decoding_options.numClasses = (
|
||||
self.tensors_decoding_options.num_classes
|
||||
)
|
||||
tensors_decoding_options.numBoxes = self.tensors_decoding_options.num_boxes
|
||||
tensors_decoding_options.numCoords = (
|
||||
self.tensors_decoding_options.num_coords
|
||||
)
|
||||
tensors_decoding_options.keypointCoordOffset = (
|
||||
self.tensors_decoding_options.keypoint_coord_offset
|
||||
)
|
||||
tensors_decoding_options.numKeypoints = (
|
||||
self.tensors_decoding_options.num_keypoints
|
||||
)
|
||||
tensors_decoding_options.numValuesPerKeypoint = (
|
||||
self.tensors_decoding_options.num_values_per_keypoint
|
||||
)
|
||||
tensors_decoding_options.xScale = self.tensors_decoding_options.x_scale
|
||||
tensors_decoding_options.yScale = self.tensors_decoding_options.y_scale
|
||||
tensors_decoding_options.wScale = self.tensors_decoding_options.w_scale
|
||||
tensors_decoding_options.hScale = self.tensors_decoding_options.h_scale
|
||||
tensors_decoding_options.applyExponentialOnBoxSize = (
|
||||
self.tensors_decoding_options.apply_exponential_on_box_size
|
||||
)
|
||||
tensors_decoding_options.sigmoidScore = (
|
||||
self.tensors_decoding_options.sigmoid_score
|
||||
)
|
||||
detector_options.tensorsDecodingOptions = tensors_decoding_options
|
||||
|
||||
# Get the object detector options flatbuffer.
|
||||
b = flatbuffers.Builder(0)
|
||||
b.Finish(detector_options.Pack(b), self._METADATA_FILE_IDENTIFIER)
|
||||
detector_options_buf = b.Output()
|
||||
|
||||
# Add the object detector options flatbuffer in custom metadata.
|
||||
custom_metadata = _metadata_fb.CustomMetadataT()
|
||||
custom_metadata.name = self.name
|
||||
custom_metadata.data = detector_options_buf
|
||||
return custom_metadata
|
||||
|
||||
|
||||
class MetadataWriter(metadata_writer.MetadataWriterBase):
|
||||
"""MetadataWriter to write the metadata into the object detector."""
|
||||
|
||||
@classmethod
|
||||
def create_for_models_with_nms(
|
||||
cls,
|
||||
model_buffer: bytearray,
|
||||
input_norm_mean: List[float],
|
||||
input_norm_std: List[float],
|
||||
labels: metadata_writer.Labels,
|
||||
score_calibration: Optional[metadata_writer.ScoreCalibration] = None,
|
||||
) -> "MetadataWriter":
|
||||
"""Creates MetadataWriter to write the metadata for object detector with postprocessing in the model.
|
||||
|
||||
This method create a metadata writer for the models with postprocessing [1].
|
||||
|
||||
The parameters required in this method are mandatory when using MediaPipe
|
||||
Tasks.
|
||||
|
||||
Example usage:
|
||||
metadata_writer = object_detector.Metadatawriter.create(model_buffer, ...)
|
||||
tflite_content, json_content = metadata_writer.populate()
|
||||
|
||||
When calling `populate` function in this class, it returns TfLite content
|
||||
and JSON content. Note that only the output TFLite is used for deployment.
|
||||
The output JSON content is used to interpret the metadata content.
|
||||
|
||||
Args:
|
||||
model_buffer: A valid flatbuffer loaded from the TFLite model file.
|
||||
input_norm_mean: the mean value used in the input tensor normalization
|
||||
[2].
|
||||
input_norm_std: the std value used in the input tensor normalizarion [2].
|
||||
labels: an instance of Labels helper class used in the output
|
||||
classification tensor [3].
|
||||
score_calibration: A container of the score calibration operation [4] in
|
||||
the classification tensor. Optional if the model does not use score
|
||||
calibration.
|
||||
[1]:
|
||||
https://github.com/tensorflow/tensorflow/blob/master/tensorflow/lite/kernels/detection_postprocess.cc
|
||||
[2]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L389
|
||||
[3]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L99
|
||||
[4]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L456
|
||||
|
||||
Returns:
|
||||
A MetadataWriter object.
|
||||
"""
|
||||
writer = metadata_writer.MetadataWriter(model_buffer)
|
||||
writer.add_general_info(_MODEL_NAME, _MODEL_DESCRIPTION)
|
||||
writer.add_image_input(input_norm_mean, input_norm_std)
|
||||
writer.add_detection_output(labels, score_calibration)
|
||||
return cls(writer)
|
||||
|
||||
@classmethod
|
||||
def create_for_models_without_nms(
|
||||
cls,
|
||||
model_buffer: bytearray,
|
||||
input_norm_mean: List[float],
|
||||
input_norm_std: List[float],
|
||||
labels: metadata_writer.Labels,
|
||||
ssd_anchors_options: SsdAnchorsOptions,
|
||||
tensors_decoding_options: TensorsDecodingOptions,
|
||||
output_tensors_order: metadata_info.RawDetectionOutputTensorsOrder = metadata_info.RawDetectionOutputTensorsOrder.UNSPECIFIED,
|
||||
) -> "MetadataWriter":
|
||||
"""Creates MetadataWriter to write the metadata for object detector without postprocessing in the model.
|
||||
|
||||
This method create a metadata writer for the models without postprocessing
|
||||
[1].
|
||||
|
||||
The parameters required in this method are mandatory when using MediaPipe
|
||||
Tasks.
|
||||
|
||||
Example usage:
|
||||
metadata_writer = object_detector.Metadatawriter.create(model_buffer, ...)
|
||||
tflite_content, json_content = metadata_writer.populate()
|
||||
|
||||
When calling `populate` function in this class, it returns TfLite content
|
||||
and JSON content. Note that only the output TFLite is used for deployment.
|
||||
The output JSON content is used to interpret the metadata content.
|
||||
|
||||
Args:
|
||||
model_buffer: A valid flatbuffer loaded from the TFLite model file.
|
||||
input_norm_mean: the mean value used in the input tensor normalization
|
||||
[2].
|
||||
input_norm_std: the std value used in the input tensor normalizarion [2].
|
||||
labels: an instance of Labels helper class used in the output
|
||||
classification tensor [3].
|
||||
ssd_anchors_options: the ssd anchors options associated to the object
|
||||
detector model.
|
||||
tensors_decoding_options: the tensors decoding options used to decode the
|
||||
object detector model output.
|
||||
output_tensors_order: the order of the output tensors.
|
||||
[1]:
|
||||
https://github.com/tensorflow/tensorflow/blob/master/tensorflow/lite/kernels/detection_postprocess.cc
|
||||
[2]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L389
|
||||
[3]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L99
|
||||
|
||||
Returns:
|
||||
A MetadataWriter object.
|
||||
"""
|
||||
writer = metadata_writer.MetadataWriter(model_buffer)
|
||||
writer.add_general_info(_MODEL_NAME, _MODEL_DESCRIPTION)
|
||||
writer.add_image_input(input_norm_mean, input_norm_std)
|
||||
writer.add_raw_detection_output(
|
||||
labels, output_tensors_order=output_tensors_order
|
||||
)
|
||||
option_md = ObjectDetectorOptionsMd(
|
||||
ssd_anchors_options, tensors_decoding_options
|
||||
)
|
||||
writer.add_custom_metadata(option_md)
|
||||
return cls(writer)
|
||||
|
||||
def populate(self) -> "tuple[bytearray, str]":
|
||||
model_buf, _ = super().populate()
|
||||
metadata_buf = metadata.get_metadata_buffer(model_buf)
|
||||
json_content = convert_to_json(metadata_buf)
|
||||
return model_buf, json_content
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Writes metadata and label file to the Text classifier models."""
|
||||
|
||||
from typing import Union
|
||||
|
||||
from mediapipe.tasks.python.metadata.metadata_writers import metadata_writer
|
||||
|
||||
_MODEL_NAME = "TextClassifier"
|
||||
_MODEL_DESCRIPTION = ("Classify the input text into a set of known categories.")
|
||||
|
||||
# The input tensor names of models created by Model Maker.
|
||||
_DEFAULT_ID_NAME = "serving_default_input_word_ids:0"
|
||||
_DEFAULT_MASK_NAME = "serving_default_input_mask:0"
|
||||
_DEFAULT_SEGMENT_ID_NAME = "serving_default_input_type_ids:0"
|
||||
|
||||
|
||||
class MetadataWriter(metadata_writer.MetadataWriterBase):
|
||||
"""MetadataWriter to write the metadata into the text classifier."""
|
||||
|
||||
@classmethod
|
||||
def create_for_regex_model(
|
||||
cls, model_buffer: bytearray,
|
||||
regex_tokenizer: metadata_writer.RegexTokenizer,
|
||||
labels: metadata_writer.Labels) -> "MetadataWriter":
|
||||
"""Creates MetadataWriter for TFLite model with regex tokentizer.
|
||||
|
||||
The parameters required in this method are mandatory when using MediaPipe
|
||||
Tasks.
|
||||
|
||||
Note that only the output TFLite is used for deployment. The output JSON
|
||||
content is used to interpret the metadata content.
|
||||
|
||||
Args:
|
||||
model_buffer: A valid flatbuffer loaded from the TFLite model file.
|
||||
regex_tokenizer: information of the regex tokenizer [1] used to process
|
||||
the input string. If the tokenizer is `BertTokenizer` [2] or
|
||||
`SentencePieceTokenizer` [3], please refer to
|
||||
`create_for_bert_model`.
|
||||
labels: an instance of Labels helper class used in the output
|
||||
classification tensor [4].
|
||||
|
||||
[1]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L500
|
||||
[2]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L477
|
||||
[3]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L485
|
||||
[4]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L99
|
||||
|
||||
Returns:
|
||||
A MetadataWriter object.
|
||||
"""
|
||||
writer = metadata_writer.MetadataWriter(model_buffer)
|
||||
writer.add_general_info(_MODEL_NAME, _MODEL_DESCRIPTION)
|
||||
writer.add_regex_text_input(regex_tokenizer)
|
||||
writer.add_classification_output(labels)
|
||||
return cls(writer)
|
||||
|
||||
@classmethod
|
||||
def create_for_bert_model(
|
||||
cls,
|
||||
model_buffer: bytearray,
|
||||
tokenizer: Union[metadata_writer.BertTokenizer,
|
||||
metadata_writer.SentencePieceTokenizer],
|
||||
labels: metadata_writer.Labels,
|
||||
ids_name: str = _DEFAULT_ID_NAME,
|
||||
mask_name: str = _DEFAULT_MASK_NAME,
|
||||
segment_name: str = _DEFAULT_SEGMENT_ID_NAME,
|
||||
) -> "MetadataWriter":
|
||||
"""Creates MetadataWriter for models with {Bert/SentencePiece}Tokenizer.
|
||||
|
||||
`ids_name`, `mask_name`, and `segment_name` correspond to the `Tensor.name`
|
||||
in the TFLite schema, which help to determine the tensor order when
|
||||
populating metadata. The default values come from Model Maker.
|
||||
|
||||
Args:
|
||||
model_buffer: valid buffer of the model file.
|
||||
tokenizer: information of the tokenizer used to process the input string,
|
||||
if any. Supported tokenziers are: `BertTokenizer` [1] and
|
||||
`SentencePieceTokenizer` [2]. If the tokenizer is `RegexTokenizer` [3],
|
||||
refer to `create_for_regex_model`.
|
||||
labels: an instance of Labels helper class used in the output
|
||||
classification tensor [4].
|
||||
ids_name: name of the ids tensor, which represents the tokenized ids of
|
||||
the input text.
|
||||
mask_name: name of the mask tensor, which represents the mask with `1` for
|
||||
real tokens and `0` for padding tokens.
|
||||
segment_name: name of the segment ids tensor, where `0` stands for the
|
||||
first sequence, and `1` stands for the second sequence if exists. [1]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L477
|
||||
[2]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L485
|
||||
[3]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L500
|
||||
[4]:
|
||||
https://github.com/google/mediapipe/blob/f8af41b1eb49ff4bdad756ff19d1d36f486be614/mediapipe/tasks/metadata/metadata_schema.fbs#L99
|
||||
|
||||
Returns:
|
||||
A MetadataWriter object.
|
||||
"""
|
||||
writer = metadata_writer.MetadataWriter(model_buffer)
|
||||
writer.add_general_info(_MODEL_NAME, _MODEL_DESCRIPTION)
|
||||
writer.add_bert_text_input(tokenizer, ids_name, mask_name, segment_name)
|
||||
writer.add_classification_output(labels)
|
||||
return cls(writer)
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Helper methods for writing metadata into TFLite models."""
|
||||
|
||||
from typing import List
|
||||
|
||||
from mediapipe.tasks.metadata import schema_py_generated as _schema_fb
|
||||
|
||||
|
||||
def get_input_tensor_names(model_buffer: bytearray) -> List[str]:
|
||||
"""Gets a list of the input tensor names."""
|
||||
subgraph = get_subgraph(model_buffer)
|
||||
tensor_names = []
|
||||
for i in range(subgraph.InputsLength()):
|
||||
index = subgraph.Inputs(i)
|
||||
tensor_names.append(subgraph.Tensors(index).Name().decode("utf-8"))
|
||||
return tensor_names
|
||||
|
||||
|
||||
def get_output_tensor_names(model_buffer: bytearray) -> List[str]:
|
||||
"""Gets a list of the output tensor names."""
|
||||
subgraph = get_subgraph(model_buffer)
|
||||
tensor_names = []
|
||||
for i in range(subgraph.OutputsLength()):
|
||||
index = subgraph.Outputs(i)
|
||||
tensor_names.append(subgraph.Tensors(index).Name().decode("utf-8"))
|
||||
return tensor_names
|
||||
|
||||
|
||||
def get_input_tensor_types(
|
||||
model_buffer: bytearray) -> List[_schema_fb.TensorType]:
|
||||
"""Gets a list of the input tensor types."""
|
||||
subgraph = get_subgraph(model_buffer)
|
||||
tensor_types = []
|
||||
for i in range(subgraph.InputsLength()):
|
||||
index = subgraph.Inputs(i)
|
||||
tensor_types.append(subgraph.Tensors(index).Type())
|
||||
return tensor_types
|
||||
|
||||
|
||||
def get_output_tensor_types(
|
||||
model_buffer: bytearray) -> List[_schema_fb.TensorType]:
|
||||
"""Gets a list of the output tensor types."""
|
||||
subgraph = get_subgraph(model_buffer)
|
||||
tensor_types = []
|
||||
for i in range(subgraph.OutputsLength()):
|
||||
index = subgraph.Outputs(i)
|
||||
tensor_types.append(subgraph.Tensors(index).Type())
|
||||
return tensor_types
|
||||
|
||||
|
||||
def get_output_tensor_indices(model_buffer: bytearray) -> List[int]:
|
||||
"""Gets a list of the output tensor indices."""
|
||||
subgraph = get_subgraph(model_buffer)
|
||||
return subgraph.OutputsAsNumpy()
|
||||
|
||||
|
||||
def get_subgraph(model_buffer: bytearray) -> _schema_fb.SubGraph:
|
||||
"""Gets the subgraph of the model.
|
||||
|
||||
TFLite does not support multi-subgraph. A model should have exactly one
|
||||
subgraph.
|
||||
|
||||
Args:
|
||||
model_buffer: valid buffer of the model file.
|
||||
|
||||
Returns:
|
||||
The subgraph of the model.
|
||||
|
||||
Raises:
|
||||
ValueError: if the model has more than one subgraph or has no subgraph.
|
||||
"""
|
||||
|
||||
model = _schema_fb.Model.GetRootAsModel(model_buffer, 0)
|
||||
|
||||
# Use the first subgraph as default. TFLite Interpreter doesn't support
|
||||
# multiple subgraphs yet, but models with mini-benchmark may have multiple
|
||||
# subgraphs for acceleration evaluation purpose.
|
||||
return model.Subgraphs(0)
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
+405
@@ -0,0 +1,405 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Tests for audio classifier."""
|
||||
|
||||
import os
|
||||
from typing import List, Tuple
|
||||
from unittest import mock
|
||||
|
||||
from absl.testing import absltest
|
||||
from absl.testing import parameterized
|
||||
import numpy as np
|
||||
from scipy.io import wavfile
|
||||
|
||||
from mediapipe.tasks.python.audio import audio_classifier
|
||||
from mediapipe.tasks.python.audio.core import audio_record
|
||||
from mediapipe.tasks.python.audio.core import audio_task_running_mode
|
||||
from mediapipe.tasks.python.components.containers import audio_data as audio_data_module
|
||||
from mediapipe.tasks.python.components.containers import classification_result as classification_result_module
|
||||
from mediapipe.tasks.python.core import base_options as base_options_module
|
||||
from mediapipe.tasks.python.test import test_utils
|
||||
|
||||
_AudioClassifier = audio_classifier.AudioClassifier
|
||||
_AudioClassifierOptions = audio_classifier.AudioClassifierOptions
|
||||
_AudioClassifierResult = classification_result_module.ClassificationResult
|
||||
_AudioData = audio_data_module.AudioData
|
||||
_AudioRecord = audio_record.AudioRecord
|
||||
_BaseOptions = base_options_module.BaseOptions
|
||||
_RUNNING_MODE = audio_task_running_mode.AudioTaskRunningMode
|
||||
|
||||
_YAMNET_MODEL_FILE = 'yamnet_audio_classifier_with_metadata.tflite'
|
||||
_YAMNET_MODEL_SAMPLE_RATE = 16000
|
||||
_TWO_HEADS_MODEL_FILE = 'two_heads.tflite'
|
||||
_SPEECH_WAV_16K_MONO = 'speech_16000_hz_mono.wav'
|
||||
_SPEECH_WAV_48K_MONO = 'speech_48000_hz_mono.wav'
|
||||
_TEST_DATA_DIR = 'mediapipe/tasks/testdata/audio'
|
||||
_TWO_HEADS_WAV_16K_MONO = 'two_heads_16000_hz_mono.wav'
|
||||
_TWO_HEADS_WAV_44K_MONO = 'two_heads_44100_hz_mono.wav'
|
||||
_YAMNET_NUM_OF_SAMPLES = 15600
|
||||
_MILLISECONDS_PER_SECOND = 1000
|
||||
|
||||
|
||||
class AudioClassifierTest(parameterized.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.yamnet_model_path = test_utils.get_test_data_path(
|
||||
os.path.join(_TEST_DATA_DIR, _YAMNET_MODEL_FILE))
|
||||
self.two_heads_model_path = test_utils.get_test_data_path(
|
||||
os.path.join(_TEST_DATA_DIR, _TWO_HEADS_MODEL_FILE))
|
||||
|
||||
def _read_wav_file(self, file_name) -> _AudioData:
|
||||
sample_rate, buffer = wavfile.read(
|
||||
test_utils.get_test_data_path(os.path.join(_TEST_DATA_DIR, file_name)))
|
||||
return _AudioData.create_from_array(
|
||||
buffer.astype(float) / np.iinfo(np.int16).max, sample_rate)
|
||||
|
||||
def _read_wav_file_as_stream(self, file_name) -> List[Tuple[_AudioData, int]]:
|
||||
sample_rate, buffer = wavfile.read(
|
||||
test_utils.get_test_data_path(os.path.join(_TEST_DATA_DIR, file_name)))
|
||||
audio_data_list = []
|
||||
start = 0
|
||||
step_size = _YAMNET_NUM_OF_SAMPLES * sample_rate / _YAMNET_MODEL_SAMPLE_RATE
|
||||
while start < len(buffer):
|
||||
end = min(start + (int)(step_size), len(buffer))
|
||||
audio_data_list.append((_AudioData.create_from_array(
|
||||
buffer[start:end].astype(float) / np.iinfo(np.int16).max,
|
||||
sample_rate), (int)(start / sample_rate * _MILLISECONDS_PER_SECOND)))
|
||||
start = end
|
||||
return audio_data_list
|
||||
|
||||
# TODO: Compares the exact score values to capture unexpected
|
||||
# changes in the inference pipeline.
|
||||
def _check_yamnet_result(
|
||||
self,
|
||||
classification_result_list: List[_AudioClassifierResult],
|
||||
expected_num_categories=521):
|
||||
self.assertLen(classification_result_list, 5)
|
||||
for idx, timestamp in enumerate([0, 975, 1950, 2925]):
|
||||
classification_result = classification_result_list[idx]
|
||||
self.assertEqual(classification_result.timestamp_ms, timestamp)
|
||||
self.assertLen(classification_result.classifications, 1)
|
||||
classifcation = classification_result.classifications[0]
|
||||
self.assertEqual(classifcation.head_index, 0)
|
||||
self.assertEqual(classifcation.head_name, 'scores')
|
||||
self.assertLen(classifcation.categories, expected_num_categories)
|
||||
audio_category = classifcation.categories[0]
|
||||
self.assertEqual(audio_category.index, 0)
|
||||
self.assertEqual(audio_category.category_name, 'Speech')
|
||||
self.assertGreater(audio_category.score, 0.9)
|
||||
|
||||
# TODO: Compares the exact score values to capture unexpected
|
||||
# changes in the inference pipeline.
|
||||
def _check_two_heads_result(
|
||||
self,
|
||||
classification_result_list: List[_AudioClassifierResult],
|
||||
first_head_expected_num_categories=521,
|
||||
second_head_expected_num_categories=5):
|
||||
self.assertGreaterEqual(len(classification_result_list), 1)
|
||||
self.assertLessEqual(len(classification_result_list), 2)
|
||||
# Checks the first result.
|
||||
classification_result = classification_result_list[0]
|
||||
self.assertEqual(classification_result.timestamp_ms, 0)
|
||||
self.assertLen(classification_result.classifications, 2)
|
||||
# Checks the first head.
|
||||
yamnet_classifcation = classification_result.classifications[0]
|
||||
self.assertEqual(yamnet_classifcation.head_index, 0)
|
||||
self.assertEqual(yamnet_classifcation.head_name, 'yamnet_classification')
|
||||
self.assertLen(yamnet_classifcation.categories,
|
||||
first_head_expected_num_categories)
|
||||
# Checks the second head.
|
||||
yamnet_category = yamnet_classifcation.categories[0]
|
||||
self.assertEqual(yamnet_category.index, 508)
|
||||
self.assertEqual(yamnet_category.category_name, 'Environmental noise')
|
||||
self.assertGreater(yamnet_category.score, 0.5)
|
||||
bird_classifcation = classification_result.classifications[1]
|
||||
self.assertEqual(bird_classifcation.head_index, 1)
|
||||
self.assertEqual(bird_classifcation.head_name, 'bird_classification')
|
||||
self.assertLen(bird_classifcation.categories,
|
||||
second_head_expected_num_categories)
|
||||
bird_category = bird_classifcation.categories[0]
|
||||
self.assertEqual(bird_category.index, 4)
|
||||
self.assertEqual(bird_category.category_name, 'Chestnut-crowned Antpitta')
|
||||
self.assertGreater(bird_category.score, 0.93)
|
||||
# Checks the second result, if present.
|
||||
if len(classification_result_list) == 2:
|
||||
classification_result = classification_result_list[1]
|
||||
self.assertEqual(classification_result.timestamp_ms, 975)
|
||||
self.assertLen(classification_result.classifications, 2)
|
||||
# Checks the first head.
|
||||
yamnet_classifcation = classification_result.classifications[0]
|
||||
self.assertEqual(yamnet_classifcation.head_index, 0)
|
||||
self.assertEqual(yamnet_classifcation.head_name, 'yamnet_classification')
|
||||
self.assertLen(yamnet_classifcation.categories,
|
||||
first_head_expected_num_categories)
|
||||
yamnet_category = yamnet_classifcation.categories[0]
|
||||
self.assertEqual(yamnet_category.index, 494)
|
||||
self.assertEqual(yamnet_category.category_name, 'Silence')
|
||||
self.assertGreater(yamnet_category.score, 0.9)
|
||||
bird_classifcation = classification_result.classifications[1]
|
||||
self.assertEqual(bird_classifcation.head_index, 1)
|
||||
self.assertEqual(bird_classifcation.head_name, 'bird_classification')
|
||||
self.assertLen(bird_classifcation.categories,
|
||||
second_head_expected_num_categories)
|
||||
# Checks the second head.
|
||||
bird_category = bird_classifcation.categories[0]
|
||||
self.assertEqual(bird_category.index, 1)
|
||||
self.assertEqual(bird_category.category_name, 'White-breasted Wood-Wren')
|
||||
self.assertGreater(bird_category.score, 0.99)
|
||||
|
||||
def test_create_from_file_succeeds_with_valid_model_path(self):
|
||||
# Creates with default option and valid model file successfully.
|
||||
with _AudioClassifier.create_from_model_path(
|
||||
self.yamnet_model_path) as classifier:
|
||||
self.assertIsInstance(classifier, _AudioClassifier)
|
||||
|
||||
def test_create_from_options_succeeds_with_valid_model_path(self):
|
||||
# Creates with options containing model file successfully.
|
||||
with _AudioClassifier.create_from_options(
|
||||
_AudioClassifierOptions(
|
||||
base_options=_BaseOptions(
|
||||
model_asset_path=self.yamnet_model_path))) as classifier:
|
||||
self.assertIsInstance(classifier, _AudioClassifier)
|
||||
|
||||
def test_create_from_options_fails_with_invalid_model_path(self):
|
||||
with self.assertRaisesRegex(
|
||||
FileNotFoundError,
|
||||
'Unable to open file at /path/to/invalid/model.tflite',
|
||||
):
|
||||
base_options = _BaseOptions(
|
||||
model_asset_path='/path/to/invalid/model.tflite')
|
||||
options = _AudioClassifierOptions(base_options=base_options)
|
||||
classifier = _AudioClassifier.create_from_options(options)
|
||||
classifier.close()
|
||||
|
||||
def test_create_from_options_succeeds_with_valid_model_content(self):
|
||||
# Creates with options containing model content successfully.
|
||||
with open(self.yamnet_model_path, 'rb') as f:
|
||||
base_options = _BaseOptions(model_asset_buffer=f.read())
|
||||
options = _AudioClassifierOptions(base_options=base_options)
|
||||
classifier = _AudioClassifier.create_from_options(options)
|
||||
self.assertIsInstance(classifier, _AudioClassifier)
|
||||
classifier.close()
|
||||
|
||||
@parameterized.parameters((_SPEECH_WAV_16K_MONO), (_SPEECH_WAV_48K_MONO))
|
||||
def test_classify_with_yamnet_model(self, audio_file):
|
||||
with _AudioClassifier.create_from_model_path(
|
||||
self.yamnet_model_path) as classifier:
|
||||
classification_result_list = classifier.classify(
|
||||
self._read_wav_file(audio_file))
|
||||
self._check_yamnet_result(classification_result_list)
|
||||
|
||||
def test_classify_with_yamnet_model_and_inputs_at_different_sample_rates(
|
||||
self):
|
||||
with _AudioClassifier.create_from_model_path(
|
||||
self.yamnet_model_path) as classifier:
|
||||
for audio_file in [_SPEECH_WAV_16K_MONO, _SPEECH_WAV_48K_MONO]:
|
||||
classification_result_list = classifier.classify(
|
||||
self._read_wav_file(audio_file))
|
||||
self._check_yamnet_result(classification_result_list)
|
||||
|
||||
@mock.patch('sounddevice.InputStream', return_value=mock.MagicMock())
|
||||
def test_create_audio_record_from_classifier_succeeds(self, _):
|
||||
# Creates AudioRecord instance using the classifier successfully.
|
||||
with _AudioClassifier.create_from_model_path(
|
||||
self.yamnet_model_path
|
||||
) as classifier:
|
||||
self.assertIsInstance(classifier, _AudioClassifier)
|
||||
record = classifier.create_audio_record(1, 16000, 16000)
|
||||
self.assertIsInstance(record, _AudioRecord)
|
||||
self.assertEqual(record.channels, 1)
|
||||
self.assertEqual(record.sampling_rate, 16000)
|
||||
self.assertEqual(record.buffer_size, 16000)
|
||||
|
||||
def test_max_result_options(self):
|
||||
with _AudioClassifier.create_from_options(
|
||||
_AudioClassifierOptions(
|
||||
base_options=_BaseOptions(model_asset_path=self.yamnet_model_path),
|
||||
max_results=1)) as classifier:
|
||||
for audio_file in [_SPEECH_WAV_16K_MONO, _SPEECH_WAV_16K_MONO]:
|
||||
classification_result_list = classifier.classify(
|
||||
self._read_wav_file(audio_file))
|
||||
self._check_yamnet_result(
|
||||
classification_result_list, expected_num_categories=1)
|
||||
|
||||
def test_score_threshold_options(self):
|
||||
with _AudioClassifier.create_from_options(
|
||||
_AudioClassifierOptions(
|
||||
base_options=_BaseOptions(model_asset_path=self.yamnet_model_path),
|
||||
score_threshold=0.9)) as classifier:
|
||||
for audio_file in [_SPEECH_WAV_16K_MONO, _SPEECH_WAV_16K_MONO]:
|
||||
classification_result_list = classifier.classify(
|
||||
self._read_wav_file(audio_file))
|
||||
self._check_yamnet_result(
|
||||
classification_result_list, expected_num_categories=1)
|
||||
|
||||
def test_allow_list_option(self):
|
||||
with _AudioClassifier.create_from_options(
|
||||
_AudioClassifierOptions(
|
||||
base_options=_BaseOptions(model_asset_path=self.yamnet_model_path),
|
||||
category_allowlist=['Speech'])) as classifier:
|
||||
for audio_file in [_SPEECH_WAV_16K_MONO, _SPEECH_WAV_16K_MONO]:
|
||||
classification_result_list = classifier.classify(
|
||||
self._read_wav_file(audio_file))
|
||||
self._check_yamnet_result(
|
||||
classification_result_list, expected_num_categories=1)
|
||||
|
||||
def test_combined_allowlist_and_denylist(self):
|
||||
# Fails with combined allowlist and denylist
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
'`category_allowlist` and `category_denylist` are mutually exclusive'
|
||||
' options',
|
||||
):
|
||||
options = _AudioClassifierOptions(
|
||||
base_options=_BaseOptions(model_asset_path=self.yamnet_model_path),
|
||||
category_allowlist=['foo'],
|
||||
category_denylist=['bar'])
|
||||
with _AudioClassifier.create_from_options(options) as unused_classifier:
|
||||
pass
|
||||
|
||||
@parameterized.parameters((_TWO_HEADS_WAV_16K_MONO),
|
||||
(_TWO_HEADS_WAV_44K_MONO))
|
||||
def test_classify_with_two_heads_model_and_inputs_at_different_sample_rates(
|
||||
self, audio_file):
|
||||
with _AudioClassifier.create_from_model_path(
|
||||
self.two_heads_model_path) as classifier:
|
||||
classification_result_list = classifier.classify(
|
||||
self._read_wav_file(audio_file))
|
||||
self._check_two_heads_result(classification_result_list)
|
||||
|
||||
def test_classify_with_two_heads_model(self):
|
||||
with _AudioClassifier.create_from_model_path(
|
||||
self.two_heads_model_path) as classifier:
|
||||
for audio_file in [_TWO_HEADS_WAV_16K_MONO, _TWO_HEADS_WAV_44K_MONO]:
|
||||
classification_result_list = classifier.classify(
|
||||
self._read_wav_file(audio_file))
|
||||
self._check_two_heads_result(classification_result_list)
|
||||
|
||||
def test_classify_with_two_heads_model_with_max_results(self):
|
||||
with _AudioClassifier.create_from_options(
|
||||
_AudioClassifierOptions(
|
||||
base_options=_BaseOptions(
|
||||
model_asset_path=self.two_heads_model_path),
|
||||
max_results=1)) as classifier:
|
||||
for audio_file in [_TWO_HEADS_WAV_16K_MONO, _TWO_HEADS_WAV_44K_MONO]:
|
||||
classification_result_list = classifier.classify(
|
||||
self._read_wav_file(audio_file))
|
||||
self._check_two_heads_result(classification_result_list, 1, 1)
|
||||
|
||||
def test_missing_sample_rate_in_audio_clips_mode(self):
|
||||
options = _AudioClassifierOptions(
|
||||
base_options=_BaseOptions(model_asset_path=self.yamnet_model_path),
|
||||
running_mode=_RUNNING_MODE.AUDIO_CLIPS)
|
||||
with self.assertRaisesRegex(ValueError,
|
||||
r'Must provide the audio sample rate'):
|
||||
with _AudioClassifier.create_from_options(options) as classifier:
|
||||
classifier.classify(_AudioData(buffer_length=100))
|
||||
|
||||
def test_missing_sample_rate_in_audio_stream_mode(self):
|
||||
options = _AudioClassifierOptions(
|
||||
base_options=_BaseOptions(model_asset_path=self.yamnet_model_path),
|
||||
running_mode=_RUNNING_MODE.AUDIO_STREAM,
|
||||
result_callback=mock.MagicMock())
|
||||
with self.assertRaisesRegex(ValueError,
|
||||
r'provide the audio sample rate in audio data'):
|
||||
with _AudioClassifier.create_from_options(options) as classifier:
|
||||
classifier.classify(_AudioData(buffer_length=100))
|
||||
|
||||
def test_missing_result_callback(self):
|
||||
options = _AudioClassifierOptions(
|
||||
base_options=_BaseOptions(model_asset_path=self.yamnet_model_path),
|
||||
running_mode=_RUNNING_MODE.AUDIO_STREAM)
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
'The audio task is in audio stream mode, a user-defined result callback'
|
||||
' must be provided.',
|
||||
):
|
||||
with _AudioClassifier.create_from_options(options) as unused_classifier:
|
||||
pass
|
||||
|
||||
def test_illegal_result_callback(self):
|
||||
options = _AudioClassifierOptions(
|
||||
base_options=_BaseOptions(model_asset_path=self.yamnet_model_path),
|
||||
running_mode=_RUNNING_MODE.AUDIO_CLIPS,
|
||||
result_callback=mock.MagicMock())
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
'The audio task is in audio clips mode, a user-defined result callback'
|
||||
" shouldn't be provided",
|
||||
):
|
||||
with _AudioClassifier.create_from_options(options) as unused_classifier:
|
||||
pass
|
||||
|
||||
def test_calling_classify_in_audio_stream_mode(self):
|
||||
options = _AudioClassifierOptions(
|
||||
base_options=_BaseOptions(model_asset_path=self.yamnet_model_path),
|
||||
running_mode=_RUNNING_MODE.AUDIO_STREAM,
|
||||
result_callback=mock.MagicMock())
|
||||
with _AudioClassifier.create_from_options(options) as classifier:
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
'Task is not initialized with the audio clips mode. Current running'
|
||||
' mode:audio stream mode',
|
||||
):
|
||||
classifier.classify(self._read_wav_file(_SPEECH_WAV_16K_MONO))
|
||||
|
||||
def test_calling_classify_async_in_audio_clips_mode(self):
|
||||
options = _AudioClassifierOptions(
|
||||
base_options=_BaseOptions(model_asset_path=self.yamnet_model_path),
|
||||
running_mode=_RUNNING_MODE.AUDIO_CLIPS)
|
||||
with _AudioClassifier.create_from_options(options) as classifier:
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
'Task is not initialized with the audio stream mode. Current running'
|
||||
' mode:audio clips mode',
|
||||
):
|
||||
classifier.classify_async(self._read_wav_file(_SPEECH_WAV_16K_MONO), 0)
|
||||
|
||||
def test_classify_async_calls_with_illegal_timestamp(self):
|
||||
options = _AudioClassifierOptions(
|
||||
base_options=_BaseOptions(model_asset_path=self.yamnet_model_path),
|
||||
running_mode=_RUNNING_MODE.AUDIO_STREAM,
|
||||
result_callback=mock.MagicMock())
|
||||
with _AudioClassifier.create_from_options(options) as classifier:
|
||||
classifier.classify_async(self._read_wav_file(_SPEECH_WAV_16K_MONO), 100)
|
||||
with self.assertRaisesRegex(
|
||||
ValueError, 'Input timestamp must be monotonically increasing.'
|
||||
):
|
||||
classifier.classify_async(self._read_wav_file(_SPEECH_WAV_16K_MONO), 0)
|
||||
|
||||
@parameterized.parameters((_SPEECH_WAV_16K_MONO), (_SPEECH_WAV_48K_MONO))
|
||||
def test_classify_async(self, audio_file):
|
||||
classification_result_list = []
|
||||
|
||||
def save_result(result: _AudioClassifierResult, timestamp_ms: int):
|
||||
result.timestamp_ms = timestamp_ms
|
||||
classification_result_list.append(result)
|
||||
|
||||
options = _AudioClassifierOptions(
|
||||
base_options=_BaseOptions(model_asset_path=self.yamnet_model_path),
|
||||
running_mode=_RUNNING_MODE.AUDIO_STREAM,
|
||||
max_results=1,
|
||||
result_callback=save_result)
|
||||
classifier = _AudioClassifier.create_from_options(options)
|
||||
audio_data_list = self._read_wav_file_as_stream(audio_file)
|
||||
for audio_data, timestamp_ms in audio_data_list:
|
||||
classifier.classify_async(audio_data, timestamp_ms)
|
||||
classifier.close()
|
||||
self._check_yamnet_result(
|
||||
classification_result_list, expected_num_categories=1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
absltest.main()
|
||||
@@ -0,0 +1,196 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Test util for MediaPipe Tasks."""
|
||||
|
||||
import difflib
|
||||
import os
|
||||
|
||||
from absl import flags
|
||||
import six
|
||||
|
||||
from google.protobuf import descriptor
|
||||
from google.protobuf import descriptor_pool
|
||||
from google.protobuf import text_format
|
||||
from mediapipe.python._framework_bindings import image as image_module
|
||||
from mediapipe.python._framework_bindings import image_frame as image_frame_module
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
_Image = image_module.Image
|
||||
_ImageFormat = image_frame_module.ImageFormat
|
||||
_RGB_CHANNELS = 3
|
||||
|
||||
|
||||
def test_srcdir():
|
||||
"""Returns the path where to look for test data files."""
|
||||
if "test_srcdir" in flags.FLAGS:
|
||||
return flags.FLAGS["test_srcdir"].value
|
||||
elif "TEST_SRCDIR" in os.environ:
|
||||
return os.environ["TEST_SRCDIR"]
|
||||
else:
|
||||
raise RuntimeError("Missing TEST_SRCDIR environment.")
|
||||
|
||||
|
||||
def get_test_data_path(file_or_dirname_path: str) -> str:
|
||||
"""Returns full test data path."""
|
||||
for directory, subdirs, files in os.walk(test_srcdir()):
|
||||
for f in subdirs + files:
|
||||
path = os.path.join(directory, f)
|
||||
if path.endswith(file_or_dirname_path):
|
||||
return path
|
||||
raise ValueError(
|
||||
"No %s in test directory: %s." % (file_or_dirname_path, test_srcdir())
|
||||
)
|
||||
|
||||
|
||||
def create_calibration_file(
|
||||
file_dir: str,
|
||||
file_name: str = "score_calibration.txt",
|
||||
content: str = "1.0,2.0,3.0,4.0",
|
||||
) -> str:
|
||||
"""Creates the calibration file."""
|
||||
calibration_file = os.path.join(file_dir, file_name)
|
||||
with open(calibration_file, mode="w") as file:
|
||||
file.write(content)
|
||||
return calibration_file
|
||||
|
||||
|
||||
def assert_proto_equals(
|
||||
self, a, b, check_initialized=True, normalize_numbers=True, msg=None
|
||||
):
|
||||
"""assert_proto_equals() is useful for unit tests.
|
||||
|
||||
It produces much more helpful output than assertEqual() for proto2 messages.
|
||||
Fails with a useful error if a and b aren't equal. Comparison of repeated
|
||||
fields matches the semantics of unittest.TestCase.assertEqual(), ie order and
|
||||
extra duplicates fields matter.
|
||||
|
||||
This is a fork of https://github.com/tensorflow/tensorflow/blob/
|
||||
master/tensorflow/python/util/protobuf/compare.py#L73. We use slightly
|
||||
different rounding cutoffs to support Mac usage.
|
||||
|
||||
Args:
|
||||
self: absltest.testing.parameterized.TestCase
|
||||
a: proto2 PB instance, or text string representing one.
|
||||
b: proto2 PB instance -- message.Message or subclass thereof.
|
||||
check_initialized: boolean, whether to fail if either a or b isn't
|
||||
initialized.
|
||||
normalize_numbers: boolean, whether to normalize types and precision of
|
||||
numbers before comparison.
|
||||
msg: if specified, is used as the error message on failure.
|
||||
"""
|
||||
pool = descriptor_pool.Default()
|
||||
if isinstance(a, six.string_types):
|
||||
a = text_format.Parse(a, b.__class__(), descriptor_pool=pool)
|
||||
|
||||
for pb in a, b:
|
||||
if check_initialized:
|
||||
errors = pb.FindInitializationErrors()
|
||||
if errors:
|
||||
self.fail("Initialization errors: %s\n%s" % (errors, pb))
|
||||
if normalize_numbers:
|
||||
_normalize_number_fields(pb)
|
||||
|
||||
a_str = text_format.MessageToString(a, descriptor_pool=pool)
|
||||
b_str = text_format.MessageToString(b, descriptor_pool=pool)
|
||||
|
||||
# Some Python versions would perform regular diff instead of multi-line
|
||||
# diff if string is longer than 2**16. We substitute this behavior
|
||||
# with a call to unified_diff instead to have easier-to-read diffs.
|
||||
# For context, see: https://bugs.python.org/issue11763.
|
||||
if len(a_str) < 2**16 and len(b_str) < 2**16:
|
||||
self.assertMultiLineEqual(a_str, b_str, msg=msg)
|
||||
else:
|
||||
diff = "".join(
|
||||
difflib.unified_diff(a_str.splitlines(True), b_str.splitlines(True))
|
||||
)
|
||||
if diff:
|
||||
self.fail("%s :\n%s" % (msg, diff))
|
||||
|
||||
|
||||
def _normalize_number_fields(pb):
|
||||
"""Normalizes types and precisions of number fields in a protocol buffer.
|
||||
|
||||
Due to subtleties in the python protocol buffer implementation, it is possible
|
||||
for values to have different types and precision depending on whether they
|
||||
were set and retrieved directly or deserialized from a protobuf. This function
|
||||
normalizes integer values to ints and longs based on width, 32-bit floats to
|
||||
five digits of precision to account for python always storing them as 64-bit,
|
||||
and ensures doubles are floating point for when they're set to integers.
|
||||
Modifies pb in place. Recurses into nested objects. https://github.com/tensorf
|
||||
low/tensorflow/blob/master/tensorflow/python/util/protobuf/compare.py#L118
|
||||
|
||||
Args:
|
||||
pb: proto2 message.
|
||||
|
||||
Returns:
|
||||
the given pb, modified in place.
|
||||
"""
|
||||
for desc, values in pb.ListFields():
|
||||
is_repeated = True
|
||||
if desc.label != descriptor.FieldDescriptor.LABEL_REPEATED:
|
||||
is_repeated = False
|
||||
values = [values]
|
||||
|
||||
normalized_values = None
|
||||
|
||||
# We force 32-bit values to int and 64-bit values to long to make
|
||||
# alternate implementations where the distinction is more significant
|
||||
# (e.g. the C++ implementation) simpler.
|
||||
if desc.type in (
|
||||
descriptor.FieldDescriptor.TYPE_INT64,
|
||||
descriptor.FieldDescriptor.TYPE_UINT64,
|
||||
descriptor.FieldDescriptor.TYPE_SINT64,
|
||||
):
|
||||
normalized_values = [int(x) for x in values]
|
||||
elif desc.type in (
|
||||
descriptor.FieldDescriptor.TYPE_INT32,
|
||||
descriptor.FieldDescriptor.TYPE_UINT32,
|
||||
descriptor.FieldDescriptor.TYPE_SINT32,
|
||||
descriptor.FieldDescriptor.TYPE_ENUM,
|
||||
):
|
||||
normalized_values = [int(x) for x in values]
|
||||
elif desc.type == descriptor.FieldDescriptor.TYPE_FLOAT:
|
||||
normalized_values = [round(x, 4) for x in values]
|
||||
elif desc.type == descriptor.FieldDescriptor.TYPE_DOUBLE:
|
||||
normalized_values = [round(float(x), 6) for x in values]
|
||||
|
||||
if normalized_values is not None:
|
||||
if is_repeated:
|
||||
pb.ClearField(desc.name)
|
||||
getattr(pb, desc.name).extend(normalized_values)
|
||||
else:
|
||||
setattr(pb, desc.name, normalized_values[0])
|
||||
|
||||
if (
|
||||
desc.type == descriptor.FieldDescriptor.TYPE_MESSAGE
|
||||
or desc.type == descriptor.FieldDescriptor.TYPE_GROUP
|
||||
):
|
||||
if (
|
||||
desc.type == descriptor.FieldDescriptor.TYPE_MESSAGE
|
||||
and desc.message_type.has_options
|
||||
and desc.message_type.GetOptions().map_entry
|
||||
):
|
||||
# This is a map, only recurse if the values have a message type.
|
||||
if (
|
||||
desc.message_type.fields_by_number[2].type
|
||||
== descriptor.FieldDescriptor.TYPE_MESSAGE
|
||||
):
|
||||
for v in six.itervalues(values):
|
||||
_normalize_number_fields(v)
|
||||
else:
|
||||
for v in values:
|
||||
# recursive step
|
||||
_normalize_number_fields(v)
|
||||
|
||||
return pb
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
# Copyright 2023 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Tests for language detector."""
|
||||
|
||||
import enum
|
||||
import os
|
||||
|
||||
from absl.testing import absltest
|
||||
from absl.testing import parameterized
|
||||
|
||||
from mediapipe.tasks.python.components.containers import category
|
||||
from mediapipe.tasks.python.components.containers import classification_result as classification_result_module
|
||||
from mediapipe.tasks.python.core import base_options as base_options_module
|
||||
from mediapipe.tasks.python.test import test_utils
|
||||
from mediapipe.tasks.python.text import language_detector
|
||||
|
||||
LanguageDetectorResult = language_detector.LanguageDetectorResult
|
||||
LanguageDetectorPrediction = (
|
||||
language_detector.LanguageDetectorResult.Detection
|
||||
)
|
||||
_BaseOptions = base_options_module.BaseOptions
|
||||
_Category = category.Category
|
||||
_Classifications = classification_result_module.Classifications
|
||||
_LanguageDetector = language_detector.LanguageDetector
|
||||
_LanguageDetectorOptions = language_detector.LanguageDetectorOptions
|
||||
|
||||
_LANGUAGE_DETECTOR_MODEL = "language_detector.tflite"
|
||||
_TEST_DATA_DIR = "mediapipe/tasks/testdata/text"
|
||||
|
||||
_SCORE_THRESHOLD = 0.3
|
||||
_EN_TEXT = "To be, or not to be, that is the question"
|
||||
_EN_EXPECTED_RESULT = LanguageDetectorResult(
|
||||
[LanguageDetectorPrediction("en", 0.999856)]
|
||||
)
|
||||
_FR_TEXT = (
|
||||
"Il y a beaucoup de bouches qui parlent et fort peu de têtes qui pensent."
|
||||
)
|
||||
_FR_EXPECTED_RESULT = LanguageDetectorResult(
|
||||
[LanguageDetectorPrediction("fr", 0.999781)]
|
||||
)
|
||||
_RU_TEXT = "это какой-то английский язык"
|
||||
_RU_EXPECTED_RESULT = LanguageDetectorResult(
|
||||
[LanguageDetectorPrediction("ru", 0.993362)]
|
||||
)
|
||||
_MIXED_TEXT = "分久必合合久必分"
|
||||
_MIXED_EXPECTED_RESULT = LanguageDetectorResult([
|
||||
LanguageDetectorPrediction("zh", 0.505424),
|
||||
LanguageDetectorPrediction("ja", 0.481617),
|
||||
])
|
||||
_TOLERANCE = 1e-6
|
||||
|
||||
|
||||
class ModelFileType(enum.Enum):
|
||||
FILE_CONTENT = 1
|
||||
FILE_NAME = 2
|
||||
|
||||
|
||||
class LanguageDetectorTest(parameterized.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.model_path = test_utils.get_test_data_path(
|
||||
os.path.join(_TEST_DATA_DIR, _LANGUAGE_DETECTOR_MODEL)
|
||||
)
|
||||
|
||||
def _expect_language_detector_result_correct(
|
||||
self,
|
||||
actual_result: LanguageDetectorResult,
|
||||
expect_result: LanguageDetectorResult,
|
||||
):
|
||||
for i, prediction in enumerate(actual_result.detections):
|
||||
expected_prediction = expect_result.detections[i]
|
||||
self.assertEqual(
|
||||
prediction.language_code,
|
||||
expected_prediction.language_code,
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
prediction.probability,
|
||||
expected_prediction.probability,
|
||||
delta=_TOLERANCE,
|
||||
)
|
||||
|
||||
def test_create_from_file_succeeds_with_valid_model_path(self):
|
||||
# Creates with default option and valid model file successfully.
|
||||
with _LanguageDetector.create_from_model_path(self.model_path) as detector:
|
||||
self.assertIsInstance(detector, _LanguageDetector)
|
||||
|
||||
def test_create_from_options_succeeds_with_valid_model_path(self):
|
||||
# Creates with options containing model file successfully.
|
||||
base_options = _BaseOptions(model_asset_path=self.model_path)
|
||||
options = _LanguageDetectorOptions(base_options=base_options)
|
||||
with _LanguageDetector.create_from_options(options) as detector:
|
||||
self.assertIsInstance(detector, _LanguageDetector)
|
||||
|
||||
def test_create_from_options_fails_with_invalid_model_path(self):
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
base_options = _BaseOptions(
|
||||
model_asset_path="/path/to/invalid/model.tflite"
|
||||
)
|
||||
options = _LanguageDetectorOptions(base_options=base_options)
|
||||
_LanguageDetector.create_from_options(options)
|
||||
|
||||
def test_create_from_options_succeeds_with_valid_model_content(self):
|
||||
# Creates with options containing model content successfully.
|
||||
with open(self.model_path, "rb") as f:
|
||||
base_options = _BaseOptions(model_asset_buffer=f.read())
|
||||
options = _LanguageDetectorOptions(base_options=base_options)
|
||||
detector = _LanguageDetector.create_from_options(options)
|
||||
self.assertIsInstance(detector, _LanguageDetector)
|
||||
|
||||
@parameterized.parameters(
|
||||
(ModelFileType.FILE_NAME, _EN_TEXT, _EN_EXPECTED_RESULT),
|
||||
(ModelFileType.FILE_CONTENT, _EN_TEXT, _EN_EXPECTED_RESULT),
|
||||
(ModelFileType.FILE_NAME, _FR_TEXT, _FR_EXPECTED_RESULT),
|
||||
(ModelFileType.FILE_CONTENT, _FR_TEXT, _FR_EXPECTED_RESULT),
|
||||
(ModelFileType.FILE_NAME, _RU_TEXT, _RU_EXPECTED_RESULT),
|
||||
(ModelFileType.FILE_CONTENT, _RU_TEXT, _RU_EXPECTED_RESULT),
|
||||
(ModelFileType.FILE_NAME, _MIXED_TEXT, _MIXED_EXPECTED_RESULT),
|
||||
(ModelFileType.FILE_CONTENT, _MIXED_TEXT, _MIXED_EXPECTED_RESULT),
|
||||
)
|
||||
def test_detect(self, model_file_type, text, expected_result):
|
||||
# Creates detector.
|
||||
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_content = f.read()
|
||||
base_options = _BaseOptions(model_asset_buffer=model_content)
|
||||
else:
|
||||
# Should never happen
|
||||
raise ValueError("model_file_type is invalid.")
|
||||
|
||||
options = _LanguageDetectorOptions(
|
||||
base_options=base_options, score_threshold=_SCORE_THRESHOLD
|
||||
)
|
||||
detector = _LanguageDetector.create_from_options(options)
|
||||
|
||||
# Performs language detection on the input.
|
||||
text_result = detector.detect(text)
|
||||
# Comparing results.
|
||||
self._expect_language_detector_result_correct(text_result, expected_result)
|
||||
# Closes the detector explicitly when the detector is not used in
|
||||
# a context.
|
||||
detector.close()
|
||||
|
||||
@parameterized.parameters(
|
||||
(ModelFileType.FILE_NAME, _EN_TEXT, _EN_EXPECTED_RESULT),
|
||||
(ModelFileType.FILE_NAME, _FR_TEXT, _FR_EXPECTED_RESULT),
|
||||
(ModelFileType.FILE_NAME, _RU_TEXT, _RU_EXPECTED_RESULT),
|
||||
(ModelFileType.FILE_CONTENT, _MIXED_TEXT, _MIXED_EXPECTED_RESULT),
|
||||
)
|
||||
def test_detect_in_context(self, model_file_type, text, expected_result):
|
||||
# Creates detector.
|
||||
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_content = f.read()
|
||||
base_options = _BaseOptions(model_asset_buffer=model_content)
|
||||
else:
|
||||
# Should never happen
|
||||
raise ValueError("model_file_type is invalid.")
|
||||
|
||||
options = _LanguageDetectorOptions(
|
||||
base_options=base_options, score_threshold=_SCORE_THRESHOLD
|
||||
)
|
||||
with _LanguageDetector.create_from_options(options) as detector:
|
||||
# Performs language detection on the input.
|
||||
text_result = detector.detect(text)
|
||||
# Comparing results.
|
||||
self._expect_language_detector_result_correct(
|
||||
text_result, expected_result
|
||||
)
|
||||
|
||||
def test_allowlist_option(self):
|
||||
# Creates detector.
|
||||
base_options = _BaseOptions(model_asset_path=self.model_path)
|
||||
options = _LanguageDetectorOptions(
|
||||
base_options=base_options,
|
||||
score_threshold=_SCORE_THRESHOLD,
|
||||
category_allowlist=["ja"],
|
||||
)
|
||||
with _LanguageDetector.create_from_options(options) as detector:
|
||||
# Performs language detection on the input.
|
||||
text_result = detector.detect(_MIXED_TEXT)
|
||||
# Comparing results.
|
||||
expected_result = LanguageDetectorResult(
|
||||
[LanguageDetectorPrediction("ja", 0.481617)]
|
||||
)
|
||||
self._expect_language_detector_result_correct(
|
||||
text_result, expected_result
|
||||
)
|
||||
|
||||
def test_denylist_option(self):
|
||||
# Creates detector.
|
||||
base_options = _BaseOptions(model_asset_path=self.model_path)
|
||||
options = _LanguageDetectorOptions(
|
||||
base_options=base_options,
|
||||
score_threshold=_SCORE_THRESHOLD,
|
||||
category_denylist=["ja"],
|
||||
)
|
||||
with _LanguageDetector.create_from_options(options) as detector:
|
||||
# Performs language detection on the input.
|
||||
text_result = detector.detect(_MIXED_TEXT)
|
||||
# Comparing results.
|
||||
expected_result = LanguageDetectorResult(
|
||||
[LanguageDetectorPrediction("zh", 0.505424)]
|
||||
)
|
||||
self._expect_language_detector_result_correct(
|
||||
text_result, expected_result
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
absltest.main()
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Tests for text classifier."""
|
||||
|
||||
import enum
|
||||
import os
|
||||
|
||||
from absl.testing import absltest
|
||||
from absl.testing import parameterized
|
||||
|
||||
from mediapipe.tasks.python.components.containers import category
|
||||
from mediapipe.tasks.python.components.containers import classification_result as classification_result_module
|
||||
from mediapipe.tasks.python.core import base_options as base_options_module
|
||||
from mediapipe.tasks.python.test import test_utils
|
||||
from mediapipe.tasks.python.text import text_classifier
|
||||
|
||||
TextClassifierResult = classification_result_module.ClassificationResult
|
||||
_BaseOptions = base_options_module.BaseOptions
|
||||
_Category = category.Category
|
||||
_Classifications = classification_result_module.Classifications
|
||||
_TextClassifier = text_classifier.TextClassifier
|
||||
_TextClassifierOptions = text_classifier.TextClassifierOptions
|
||||
|
||||
_BERT_MODEL_FILE = 'bert_text_classifier.tflite'
|
||||
_REGEX_MODEL_FILE = 'test_model_text_classifier_with_regex_tokenizer.tflite'
|
||||
_TEST_DATA_DIR = 'mediapipe/tasks/testdata/text'
|
||||
|
||||
_NEGATIVE_TEXT = 'What a waste of my time.'
|
||||
_POSITIVE_TEXT = ('This is the best movie I’ve seen in recent years.'
|
||||
'Strongly recommend it!')
|
||||
|
||||
_BERT_NEGATIVE_RESULTS = TextClassifierResult(
|
||||
classifications=[
|
||||
_Classifications(
|
||||
categories=[
|
||||
_Category(
|
||||
index=0,
|
||||
score=0.9995,
|
||||
display_name='',
|
||||
category_name='negative'),
|
||||
_Category(
|
||||
index=1,
|
||||
score=0.0005,
|
||||
display_name='',
|
||||
category_name='positive')
|
||||
],
|
||||
head_index=0,
|
||||
head_name='probability')
|
||||
],
|
||||
timestamp_ms=0)
|
||||
_BERT_POSITIVE_RESULTS = TextClassifierResult(
|
||||
classifications=[
|
||||
_Classifications(
|
||||
categories=[
|
||||
_Category(
|
||||
index=1,
|
||||
score=0.9994,
|
||||
display_name='',
|
||||
category_name='positive',
|
||||
),
|
||||
_Category(
|
||||
index=0,
|
||||
score=0.0006,
|
||||
display_name='',
|
||||
category_name='negative',
|
||||
),
|
||||
],
|
||||
head_index=0,
|
||||
head_name='probability',
|
||||
)
|
||||
],
|
||||
timestamp_ms=0,
|
||||
)
|
||||
_REGEX_NEGATIVE_RESULTS = TextClassifierResult(
|
||||
classifications=[
|
||||
_Classifications(
|
||||
categories=[
|
||||
_Category(
|
||||
index=0,
|
||||
score=0.81313,
|
||||
display_name='',
|
||||
category_name='Negative'),
|
||||
_Category(
|
||||
index=1,
|
||||
score=0.1868704,
|
||||
display_name='',
|
||||
category_name='Positive')
|
||||
],
|
||||
head_index=0,
|
||||
head_name='probability')
|
||||
],
|
||||
timestamp_ms=0)
|
||||
_REGEX_POSITIVE_RESULTS = TextClassifierResult(
|
||||
classifications=[
|
||||
_Classifications(
|
||||
categories=[
|
||||
_Category(
|
||||
index=1,
|
||||
score=0.5134273,
|
||||
display_name='',
|
||||
category_name='Positive'),
|
||||
_Category(
|
||||
index=0,
|
||||
score=0.486573,
|
||||
display_name='',
|
||||
category_name='Negative')
|
||||
],
|
||||
head_index=0,
|
||||
head_name='probability')
|
||||
],
|
||||
timestamp_ms=0)
|
||||
|
||||
|
||||
class ModelFileType(enum.Enum):
|
||||
FILE_CONTENT = 1
|
||||
FILE_NAME = 2
|
||||
|
||||
|
||||
class TextClassifierTest(parameterized.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.model_path = test_utils.get_test_data_path(
|
||||
os.path.join(_TEST_DATA_DIR, _BERT_MODEL_FILE))
|
||||
|
||||
def assertTextClassifierResultEquals(
|
||||
self,
|
||||
result: TextClassifierResult,
|
||||
expected_result: TextClassifierResult
|
||||
):
|
||||
self.assertEqual(result.timestamp_ms, expected_result.timestamp_ms)
|
||||
self.assertLen(result.classifications, len(expected_result.classifications))
|
||||
for i, actual_classification in enumerate(result.classifications):
|
||||
expected_classification = expected_result.classifications[i]
|
||||
self.assertEqual(
|
||||
actual_classification.head_index, expected_classification.head_index
|
||||
)
|
||||
self.assertEqual(
|
||||
actual_classification.head_name, expected_classification.head_name
|
||||
)
|
||||
self.assertLen(
|
||||
actual_classification.categories,
|
||||
len(expected_classification.categories),
|
||||
)
|
||||
for j, actual_category in enumerate(actual_classification.categories):
|
||||
expected_category = expected_classification.categories[j]
|
||||
self.assertEqual(actual_category.index, expected_category.index)
|
||||
self.assertEqual(
|
||||
actual_category.display_name or '', expected_category.display_name
|
||||
)
|
||||
self.assertEqual(
|
||||
actual_category.category_name, expected_category.category_name
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
actual_category.score, expected_category.score, delta=1e-4
|
||||
)
|
||||
|
||||
def test_create_from_file_succeeds_with_valid_model_path(self):
|
||||
# Creates with default option and valid model file successfully.
|
||||
with _TextClassifier.create_from_model_path(self.model_path) as classifier:
|
||||
self.assertIsInstance(classifier, _TextClassifier)
|
||||
|
||||
def test_create_from_options_succeeds_with_valid_model_path(self):
|
||||
# Creates with options containing model file successfully.
|
||||
base_options = _BaseOptions(model_asset_path=self.model_path)
|
||||
options = _TextClassifierOptions(base_options=base_options)
|
||||
with _TextClassifier.create_from_options(options) as classifier:
|
||||
self.assertIsInstance(classifier, _TextClassifier)
|
||||
|
||||
def test_create_from_options_fails_with_invalid_model_path(self):
|
||||
with self.assertRaisesRegex(
|
||||
FileNotFoundError,
|
||||
'Unable to open file at /path/to/invalid/model.tflite',
|
||||
):
|
||||
base_options = _BaseOptions(
|
||||
model_asset_path='/path/to/invalid/model.tflite')
|
||||
options = _TextClassifierOptions(base_options=base_options)
|
||||
_TextClassifier.create_from_options(options)
|
||||
|
||||
def test_create_from_options_succeeds_with_valid_model_content(self):
|
||||
# Creates with options containing model content successfully.
|
||||
with open(self.model_path, 'rb') as f:
|
||||
base_options = _BaseOptions(model_asset_buffer=f.read())
|
||||
options = _TextClassifierOptions(base_options=base_options)
|
||||
classifier = _TextClassifier.create_from_options(options)
|
||||
self.assertIsInstance(classifier, _TextClassifier)
|
||||
|
||||
def test_create_from_options_succeeds_with_allow_list(self):
|
||||
base_options = _BaseOptions(model_asset_path=self.model_path)
|
||||
options = _TextClassifierOptions(
|
||||
base_options=base_options, category_allowlist=['positive']
|
||||
)
|
||||
with _TextClassifier.create_from_options(options) as classifier:
|
||||
self.assertIsInstance(classifier, _TextClassifier)
|
||||
|
||||
def test_create_from_options_succeeds_with_deny_list(self):
|
||||
base_options = _BaseOptions(model_asset_path=self.model_path)
|
||||
options = _TextClassifierOptions(
|
||||
base_options=base_options, category_denylist=['negative']
|
||||
)
|
||||
with _TextClassifier.create_from_options(options) as classifier:
|
||||
self.assertIsInstance(classifier, _TextClassifier)
|
||||
|
||||
def test_create_from_options_succeeds_with_display_names_locale(self):
|
||||
base_options = _BaseOptions(model_asset_path=self.model_path)
|
||||
options = _TextClassifierOptions(
|
||||
base_options=base_options, display_names_locale='en'
|
||||
)
|
||||
with _TextClassifier.create_from_options(options) as classifier:
|
||||
self.assertIsInstance(classifier, _TextClassifier)
|
||||
|
||||
@parameterized.parameters(
|
||||
(ModelFileType.FILE_NAME, _BERT_MODEL_FILE, _NEGATIVE_TEXT,
|
||||
_BERT_NEGATIVE_RESULTS), (ModelFileType.FILE_CONTENT, _BERT_MODEL_FILE,
|
||||
_NEGATIVE_TEXT, _BERT_NEGATIVE_RESULTS),
|
||||
(ModelFileType.FILE_NAME, _BERT_MODEL_FILE, _POSITIVE_TEXT,
|
||||
_BERT_POSITIVE_RESULTS), (ModelFileType.FILE_CONTENT, _BERT_MODEL_FILE,
|
||||
_POSITIVE_TEXT, _BERT_POSITIVE_RESULTS),
|
||||
(ModelFileType.FILE_NAME, _REGEX_MODEL_FILE, _NEGATIVE_TEXT,
|
||||
_REGEX_NEGATIVE_RESULTS), (ModelFileType.FILE_CONTENT, _REGEX_MODEL_FILE,
|
||||
_NEGATIVE_TEXT, _REGEX_NEGATIVE_RESULTS),
|
||||
(ModelFileType.FILE_NAME, _REGEX_MODEL_FILE, _POSITIVE_TEXT,
|
||||
_REGEX_POSITIVE_RESULTS), (ModelFileType.FILE_CONTENT, _REGEX_MODEL_FILE,
|
||||
_POSITIVE_TEXT, _REGEX_POSITIVE_RESULTS))
|
||||
def test_classify(self, model_file_type, model_name, text,
|
||||
expected_classification_result):
|
||||
# Creates classifier.
|
||||
model_path = test_utils.get_test_data_path(
|
||||
os.path.join(_TEST_DATA_DIR, model_name))
|
||||
if model_file_type is ModelFileType.FILE_NAME:
|
||||
base_options = _BaseOptions(model_asset_path=model_path)
|
||||
elif model_file_type is ModelFileType.FILE_CONTENT:
|
||||
with open(model_path, 'rb') as f:
|
||||
model_content = f.read()
|
||||
base_options = _BaseOptions(model_asset_buffer=model_content)
|
||||
else:
|
||||
# Should never happen
|
||||
raise ValueError('model_file_type is invalid.')
|
||||
|
||||
options = _TextClassifierOptions(base_options=base_options)
|
||||
classifier = _TextClassifier.create_from_options(options)
|
||||
|
||||
# Performs text classification on the input.
|
||||
text_result = classifier.classify(text)
|
||||
# Comparing results.
|
||||
self.assertTextClassifierResultEquals(
|
||||
text_result, expected_classification_result
|
||||
)
|
||||
# Closes the classifier explicitly when the classifier is not used in
|
||||
# a context.
|
||||
classifier.close()
|
||||
|
||||
@parameterized.parameters((ModelFileType.FILE_NAME, _BERT_MODEL_FILE,
|
||||
_NEGATIVE_TEXT, _BERT_NEGATIVE_RESULTS),
|
||||
(ModelFileType.FILE_CONTENT, _BERT_MODEL_FILE,
|
||||
_NEGATIVE_TEXT, _BERT_NEGATIVE_RESULTS))
|
||||
def test_classify_in_context(self, model_file_type, model_name, text,
|
||||
expected_classification_result):
|
||||
# Creates classifier.
|
||||
model_path = test_utils.get_test_data_path(
|
||||
os.path.join(_TEST_DATA_DIR, model_name))
|
||||
if model_file_type is ModelFileType.FILE_NAME:
|
||||
base_options = _BaseOptions(model_asset_path=model_path)
|
||||
elif model_file_type is ModelFileType.FILE_CONTENT:
|
||||
with open(model_path, 'rb') as f:
|
||||
model_content = f.read()
|
||||
base_options = _BaseOptions(model_asset_buffer=model_content)
|
||||
else:
|
||||
# Should never happen
|
||||
raise ValueError('model_file_type is invalid.')
|
||||
|
||||
options = _TextClassifierOptions(base_options=base_options)
|
||||
|
||||
with _TextClassifier.create_from_options(options) as classifier:
|
||||
# Performs text classification on the input.
|
||||
text_result = classifier.classify(text)
|
||||
# Comparing results.
|
||||
self.assertTextClassifierResultEquals(
|
||||
text_result, expected_classification_result
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
absltest.main()
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Tests for text embedder."""
|
||||
|
||||
import enum
|
||||
import os
|
||||
|
||||
from absl.testing import absltest
|
||||
from absl.testing import parameterized
|
||||
import numpy as np
|
||||
|
||||
from mediapipe.tasks.python.components.containers import embedding_result as embedding_result_module
|
||||
from mediapipe.tasks.python.core import base_options as base_options_module
|
||||
from mediapipe.tasks.python.test import test_utils
|
||||
from mediapipe.tasks.python.text import text_embedder
|
||||
|
||||
_BaseOptions = base_options_module.BaseOptions
|
||||
_Embedding = embedding_result_module.Embedding
|
||||
_TextEmbedder = text_embedder.TextEmbedder
|
||||
_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
|
||||
# Tolerance for cosine similarity evaluation.
|
||||
_SIMILARITY_TOLERANCE = 1e-3
|
||||
|
||||
|
||||
class ModelFileType(enum.Enum):
|
||||
FILE_CONTENT = 1
|
||||
FILE_NAME = 2
|
||||
|
||||
|
||||
class TextEmbedderTest(parameterized.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.model_path = test_utils.get_test_data_path(
|
||||
os.path.join(_TEST_DATA_DIR, _BERT_MODEL_FILE))
|
||||
|
||||
def test_create_from_file_succeeds_with_valid_model_path(self):
|
||||
# Creates with default option and valid model file successfully.
|
||||
with _TextEmbedder.create_from_model_path(self.model_path) as embedder:
|
||||
self.assertIsInstance(embedder, _TextEmbedder)
|
||||
|
||||
def test_create_from_options_succeeds_with_valid_model_path(self):
|
||||
# Creates with options containing model file successfully.
|
||||
base_options = _BaseOptions(model_asset_path=self.model_path)
|
||||
options = _TextEmbedderOptions(base_options=base_options)
|
||||
with _TextEmbedder.create_from_options(options) as embedder:
|
||||
self.assertIsInstance(embedder, _TextEmbedder)
|
||||
|
||||
def test_create_from_options_fails_with_invalid_model_path(self):
|
||||
with self.assertRaisesRegex(
|
||||
FileNotFoundError,
|
||||
'Unable to open file at /path/to/invalid/model.tflite',
|
||||
):
|
||||
base_options = _BaseOptions(
|
||||
model_asset_path='/path/to/invalid/model.tflite')
|
||||
options = _TextEmbedderOptions(base_options=base_options)
|
||||
_TextEmbedder.create_from_options(options)
|
||||
|
||||
def test_create_from_options_succeeds_with_valid_model_content(self):
|
||||
# Creates with options containing model content successfully.
|
||||
with open(self.model_path, 'rb') as f:
|
||||
base_options = _BaseOptions(model_asset_buffer=f.read())
|
||||
options = _TextEmbedderOptions(base_options=base_options)
|
||||
embedder = _TextEmbedder.create_from_options(options)
|
||||
self.assertIsInstance(embedder, _TextEmbedder)
|
||||
|
||||
def _check_embedding_value(self, result, expected_first_value):
|
||||
# Check embedding first value.
|
||||
self.assertAlmostEqual(
|
||||
result.embeddings[0].embedding[0], expected_first_value, delta=_EPSILON)
|
||||
|
||||
def _check_embedding_size(self, result, quantize, expected_embedding_size):
|
||||
# Check embedding size.
|
||||
self.assertLen(result.embeddings, 1)
|
||||
embedding_result = result.embeddings[0]
|
||||
self.assertLen(embedding_result.embedding, expected_embedding_size)
|
||||
if quantize:
|
||||
self.assertEqual(embedding_result.embedding.dtype, np.uint8)
|
||||
else:
|
||||
self.assertEqual(embedding_result.embedding.dtype, float)
|
||||
|
||||
def _check_cosine_similarity(self, result0, result1, expected_similarity):
|
||||
# Checks cosine similarity.
|
||||
similarity = _TextEmbedder.cosine_similarity(result0.embeddings[0],
|
||||
result1.embeddings[0])
|
||||
self.assertAlmostEqual(
|
||||
similarity, expected_similarity, delta=_SIMILARITY_TOLERANCE)
|
||||
|
||||
@parameterized.parameters(
|
||||
(
|
||||
False,
|
||||
False,
|
||||
_BERT_MODEL_FILE,
|
||||
ModelFileType.FILE_NAME,
|
||||
0.9624276,∂ç
|
||||
512,
|
||||
(21.2054, 19.6843),
|
||||
),
|
||||
(
|
||||
True,
|
||||
False,
|
||||
_BERT_MODEL_FILE,
|
||||
ModelFileType.FILE_NAME,
|
||||
0.9624276,
|
||||
512,
|
||||
(0.062578, 0.067393),
|
||||
),
|
||||
(
|
||||
False,
|
||||
False,
|
||||
_REGEX_MODEL_FILE,
|
||||
ModelFileType.FILE_NAME,
|
||||
0.999937,
|
||||
16,
|
||||
(0.0309356, 0.0312863),
|
||||
),
|
||||
(
|
||||
True,
|
||||
False,
|
||||
_REGEX_MODEL_FILE,
|
||||
ModelFileType.FILE_CONTENT,
|
||||
0.999937,
|
||||
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):
|
||||
# Creates embedder.
|
||||
model_path = test_utils.get_test_data_path(
|
||||
os.path.join(_TEST_DATA_DIR, model_name))
|
||||
if model_file_type is ModelFileType.FILE_NAME:
|
||||
base_options = _BaseOptions(model_asset_path=model_path)
|
||||
elif model_file_type is ModelFileType.FILE_CONTENT:
|
||||
with open(model_path, 'rb') as f:
|
||||
model_content = f.read()
|
||||
base_options = _BaseOptions(model_asset_buffer=model_content)
|
||||
else:
|
||||
# Should never happen
|
||||
raise ValueError('model_file_type is invalid.')
|
||||
|
||||
options = _TextEmbedderOptions(
|
||||
base_options=base_options, l2_normalize=l2_normalize, quantize=quantize)
|
||||
embedder = _TextEmbedder.create_from_options(options)
|
||||
|
||||
# Extracts both embeddings.
|
||||
positive_text0 = "it's a charming and often affecting journey"
|
||||
positive_text1 = 'what a great and fantastic trip'
|
||||
|
||||
result0 = embedder.embed(positive_text0)
|
||||
result1 = embedder.embed(positive_text1)
|
||||
|
||||
# Checks embeddings and cosine similarity.
|
||||
expected_result0_value, expected_result1_value = expected_first_values
|
||||
self._check_embedding_size(result0, quantize, expected_size)
|
||||
self._check_embedding_size(result1, quantize, expected_size)
|
||||
self._check_embedding_value(result0, expected_result0_value)
|
||||
self._check_embedding_value(result1, expected_result1_value)
|
||||
self._check_cosine_similarity(result0, result1, expected_similarity)
|
||||
# Closes the embedder explicitly when the embedder is not used in
|
||||
# a context.
|
||||
embedder.close()
|
||||
|
||||
@parameterized.parameters(
|
||||
(
|
||||
False,
|
||||
False,
|
||||
_BERT_MODEL_FILE,
|
||||
ModelFileType.FILE_NAME,
|
||||
0.962427,
|
||||
512,
|
||||
(21.2054, 19.684337),
|
||||
),
|
||||
(
|
||||
True,
|
||||
False,
|
||||
_BERT_MODEL_FILE,
|
||||
ModelFileType.FILE_NAME,
|
||||
0.962427,
|
||||
512,
|
||||
(0.0625787, 0.0673937),
|
||||
),
|
||||
(
|
||||
False,
|
||||
False,
|
||||
_REGEX_MODEL_FILE,
|
||||
ModelFileType.FILE_NAME,
|
||||
0.999937,
|
||||
16,
|
||||
(0.0309356, 0.0312863),
|
||||
),
|
||||
(
|
||||
True,
|
||||
False,
|
||||
_REGEX_MODEL_FILE,
|
||||
ModelFileType.FILE_CONTENT,
|
||||
0.999937,
|
||||
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,
|
||||
expected_first_values):
|
||||
# Creates embedder.
|
||||
model_path = test_utils.get_test_data_path(
|
||||
os.path.join(_TEST_DATA_DIR, model_name))
|
||||
if model_file_type is ModelFileType.FILE_NAME:
|
||||
base_options = _BaseOptions(model_asset_path=model_path)
|
||||
elif model_file_type is ModelFileType.FILE_CONTENT:
|
||||
with open(model_path, 'rb') as f:
|
||||
model_content = f.read()
|
||||
base_options = _BaseOptions(model_asset_buffer=model_content)
|
||||
else:
|
||||
# Should never happen
|
||||
raise ValueError('model_file_type is invalid.')
|
||||
|
||||
options = _TextEmbedderOptions(
|
||||
base_options=base_options, l2_normalize=l2_normalize, quantize=quantize)
|
||||
with _TextEmbedder.create_from_options(options) as embedder:
|
||||
# Extracts both embeddings.
|
||||
positive_text0 = "it's a charming and often affecting journey"
|
||||
positive_text1 = 'what a great and fantastic trip'
|
||||
|
||||
result0 = embedder.embed(positive_text0)
|
||||
result1 = embedder.embed(positive_text1)
|
||||
|
||||
# Checks embeddings and cosine similarity.
|
||||
expected_result0_value, expected_result1_value = expected_first_values
|
||||
self._check_embedding_size(result0, quantize, expected_size)
|
||||
self._check_embedding_size(result1, quantize, expected_size)
|
||||
self._check_embedding_value(result0, expected_result0_value)
|
||||
self._check_embedding_value(result1, expected_result1_value)
|
||||
self._check_cosine_similarity(result0, result1, expected_similarity)
|
||||
|
||||
@parameterized.parameters(
|
||||
# TODO: The similarity should likely be lower
|
||||
(_BERT_MODEL_FILE, 0.98103),
|
||||
(_USE_MODEL_FILE, 0.780334),
|
||||
)
|
||||
def test_embed_with_different_themes(self, model_file, expected_similarity):
|
||||
# Creates embedder.
|
||||
model_path = test_utils.get_test_data_path(
|
||||
os.path.join(_TEST_DATA_DIR, model_file)
|
||||
)
|
||||
base_options = _BaseOptions(model_asset_path=model_path)
|
||||
options = _TextEmbedderOptions(base_options=base_options)
|
||||
embedder = _TextEmbedder.create_from_options(options)
|
||||
|
||||
# Extracts both embeddings.
|
||||
text0 = (
|
||||
'When you go to this restaurant, they hold the pancake upside-down '
|
||||
"before they hand it to you. It's a great gimmick."
|
||||
)
|
||||
result0 = embedder.embed(text0)
|
||||
|
||||
text1 = "Let's make a plan to steal the declaration of independence."
|
||||
result1 = embedder.embed(text1)
|
||||
|
||||
similarity = _TextEmbedder.cosine_similarity(
|
||||
result0.embeddings[0], result1.embeddings[0]
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(
|
||||
similarity, expected_similarity, delta=_SIMILARITY_TOLERANCE
|
||||
)
|
||||
|
||||
# Closes the embedder explicitly when the embedder is not used in
|
||||
# a context.
|
||||
embedder.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
absltest.main()
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright 2022 The MediaPipe Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user