Merge pull request #4194 from priankakariatyml:ios-image-classifier-tests
PiperOrigin-RevId: 519907148
This commit is contained in:
commit
59b3150fff
|
@ -107,18 +107,18 @@ using ::mediapipe::InputStreamInfo;
|
||||||
for (NSString *inputStream in self.inputStreams) {
|
for (NSString *inputStream in self.inputStreams) {
|
||||||
graphConfig.add_input_stream(inputStream.cppString);
|
graphConfig.add_input_stream(inputStream.cppString);
|
||||||
|
|
||||||
NSString *strippedInputStream = [MPPTaskInfo stripTagIndex:inputStream];
|
|
||||||
flowLimitCalculatorNode->add_input_stream(strippedInputStream.cppString);
|
|
||||||
|
|
||||||
NSString *taskInputStream = [MPPTaskInfo addStreamNamePrefix:inputStream];
|
NSString *taskInputStream = [MPPTaskInfo addStreamNamePrefix:inputStream];
|
||||||
taskSubgraphNode->add_input_stream(taskInputStream.cppString);
|
taskSubgraphNode->add_input_stream(taskInputStream.cppString);
|
||||||
|
|
||||||
|
NSString *strippedInputStream = [MPPTaskInfo stripTagIndex:inputStream];
|
||||||
|
flowLimitCalculatorNode->add_input_stream(strippedInputStream.cppString);
|
||||||
|
|
||||||
NSString *strippedTaskInputStream = [MPPTaskInfo stripTagIndex:taskInputStream];
|
NSString *strippedTaskInputStream = [MPPTaskInfo stripTagIndex:taskInputStream];
|
||||||
flowLimitCalculatorNode->add_output_stream(strippedTaskInputStream.cppString);
|
flowLimitCalculatorNode->add_output_stream(strippedTaskInputStream.cppString);
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString *firstOutputStream = self.outputStreams[0];
|
NSString *strippedFirstOutputStream = [MPPTaskInfo stripTagIndex:self.outputStreams[0]];
|
||||||
auto finishedOutputStream = "FINISHED:" + firstOutputStream.cppString;
|
auto finishedOutputStream = "FINISHED:" + strippedFirstOutputStream.cppString;
|
||||||
flowLimitCalculatorNode->add_input_stream(finishedOutputStream);
|
flowLimitCalculatorNode->add_input_stream(finishedOutputStream);
|
||||||
|
|
||||||
return graphConfig;
|
return graphConfig;
|
||||||
|
|
|
@ -20,6 +20,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
@interface MPPBaseOptions (Helpers)
|
@interface MPPBaseOptions (Helpers)
|
||||||
|
|
||||||
- (void)copyToProto:(mediapipe::tasks::core::proto::BaseOptions *)baseOptionsProto;
|
- (void)copyToProto:(mediapipe::tasks::core::proto::BaseOptions *)baseOptionsProto;
|
||||||
|
- (void)copyToProto:(mediapipe::tasks::core::proto::BaseOptions *)baseOptionsProto
|
||||||
|
withUseStreamMode:(BOOL)useStreamMode;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,11 @@ using BaseOptionsProto = ::mediapipe::tasks::core::proto::BaseOptions;
|
||||||
|
|
||||||
@implementation MPPBaseOptions (Helpers)
|
@implementation MPPBaseOptions (Helpers)
|
||||||
|
|
||||||
|
- (void)copyToProto:(BaseOptionsProto *)baseOptionsProto withUseStreamMode:(BOOL)useStreamMode {
|
||||||
|
[self copyToProto:baseOptionsProto];
|
||||||
|
baseOptionsProto->set_use_stream_mode(useStreamMode);
|
||||||
|
}
|
||||||
|
|
||||||
- (void)copyToProto:(BaseOptionsProto *)baseOptionsProto {
|
- (void)copyToProto:(BaseOptionsProto *)baseOptionsProto {
|
||||||
baseOptionsProto->Clear();
|
baseOptionsProto->Clear();
|
||||||
|
|
||||||
|
|
55
mediapipe/tasks/ios/test/vision/image_classifier/BUILD
Normal file
55
mediapipe/tasks/ios/test/vision/image_classifier/BUILD
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
load("@build_bazel_rules_apple//apple:ios.bzl", "ios_unit_test")
|
||||||
|
load(
|
||||||
|
"//mediapipe/tasks:ios/ios.bzl",
|
||||||
|
"MPP_TASK_MINIMUM_OS_VERSION",
|
||||||
|
)
|
||||||
|
load(
|
||||||
|
"@org_tensorflow//tensorflow/lite:special_rules.bzl",
|
||||||
|
"tflite_ios_lab_runner",
|
||||||
|
)
|
||||||
|
|
||||||
|
package(default_visibility = ["//mediapipe/tasks:internal"])
|
||||||
|
|
||||||
|
licenses(["notice"])
|
||||||
|
|
||||||
|
# Default tags for filtering iOS targets. Targets are restricted to Apple platforms.
|
||||||
|
TFL_DEFAULT_TAGS = [
|
||||||
|
"apple",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Following sanitizer tests are not supported by iOS test targets.
|
||||||
|
TFL_DISABLED_SANITIZER_TAGS = [
|
||||||
|
"noasan",
|
||||||
|
"nomsan",
|
||||||
|
"notsan",
|
||||||
|
]
|
||||||
|
|
||||||
|
objc_library(
|
||||||
|
name = "MPPImageClassifierObjcTestLibrary",
|
||||||
|
testonly = 1,
|
||||||
|
srcs = ["MPPImageClassifierTests.m"],
|
||||||
|
copts = [
|
||||||
|
"-ObjC++",
|
||||||
|
"-std=c++17",
|
||||||
|
"-x objective-c++",
|
||||||
|
],
|
||||||
|
data = [
|
||||||
|
"//mediapipe/tasks/testdata/vision:test_images",
|
||||||
|
"//mediapipe/tasks/testdata/vision:test_models",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//mediapipe/tasks/ios/common:MPPCommon",
|
||||||
|
"//mediapipe/tasks/ios/test/vision/utils:MPPImageTestUtils",
|
||||||
|
"//mediapipe/tasks/ios/vision/image_classifier:MPPImageClassifier",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
ios_unit_test(
|
||||||
|
name = "MPPImageClassifierObjcTest",
|
||||||
|
minimum_os_version = MPP_TASK_MINIMUM_OS_VERSION,
|
||||||
|
runner = tflite_ios_lab_runner("IOS_LATEST"),
|
||||||
|
tags = TFL_DEFAULT_TAGS + TFL_DISABLED_SANITIZER_TAGS,
|
||||||
|
deps = [
|
||||||
|
":MPPImageClassifierObjcTestLibrary",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,675 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#import <XCTest/XCTest.h>
|
||||||
|
|
||||||
|
#import "mediapipe/tasks/ios/common/sources/MPPCommon.h"
|
||||||
|
#import "mediapipe/tasks/ios/test/vision/utils/sources/MPPImage+TestUtils.h"
|
||||||
|
#import "mediapipe/tasks/ios/vision/image_classifier/sources/MPPImageClassifier.h"
|
||||||
|
|
||||||
|
static NSString *kFloatModelName = @"mobilenet_v2_1.0_224";
|
||||||
|
static NSString *const kQuantizedModelName = @"mobilenet_v1_0.25_224_quant";
|
||||||
|
static NSDictionary *const kBurgerImage = @{@"name" : @"burger", @"type" : @"jpg"};
|
||||||
|
static NSDictionary *const kBurgerRotatedImage = @{@"name" : @"burger_rotated", @"type" : @"jpg"};
|
||||||
|
static NSDictionary *const kMultiObjectsImage = @{@"name" : @"multi_objects", @"type" : @"jpg"};
|
||||||
|
static NSDictionary *const kMultiObjectsRotatedImage =
|
||||||
|
@{@"name" : @"multi_objects_rotated", @"type" : @"jpg"};
|
||||||
|
static const int kMobileNetCategoriesCount = 1001;
|
||||||
|
static NSString *const kExpectedErrorDomain = @"com.google.mediapipe.tasks";
|
||||||
|
|
||||||
|
#define AssertEqualErrors(error, expectedError) \
|
||||||
|
XCTAssertNotNil(error); \
|
||||||
|
XCTAssertEqualObjects(error.domain, expectedError.domain); \
|
||||||
|
XCTAssertEqual(error.code, expectedError.code); \
|
||||||
|
XCTAssertNotEqual( \
|
||||||
|
[error.localizedDescription rangeOfString:expectedError.localizedDescription].location, \
|
||||||
|
NSNotFound)
|
||||||
|
|
||||||
|
#define AssertEqualCategoryArrays(categories, expectedCategories) \
|
||||||
|
XCTAssertEqual(categories.count, expectedCategories.count); \
|
||||||
|
for (int i = 0; i < categories.count; i++) { \
|
||||||
|
XCTAssertEqual(categories[i].index, expectedCategories[i].index, @"index i = %d", i); \
|
||||||
|
XCTAssertEqualWithAccuracy(categories[i].score, expectedCategories[i].score, 1e-3, \
|
||||||
|
@"index i = %d", i); \
|
||||||
|
XCTAssertEqualObjects(categories[i].categoryName, expectedCategories[i].categoryName, \
|
||||||
|
@"index i = %d", i); \
|
||||||
|
XCTAssertEqualObjects(categories[i].displayName, expectedCategories[i].displayName, \
|
||||||
|
@"index i = %d", i); \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define AssertImageClassifierResultHasOneHead(imageClassifierResult) \
|
||||||
|
XCTAssertNotNil(imageClassifierResult); \
|
||||||
|
XCTAssertNotNil(imageClassifierResult.classificationResult); \
|
||||||
|
XCTAssertEqual(imageClassifierResult.classificationResult.classifications.count, 1); \
|
||||||
|
XCTAssertEqual(imageClassifierResult.classificationResult.classifications[0].headIndex, 0);
|
||||||
|
|
||||||
|
@interface MPPImageClassifierTests : XCTestCase
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation MPPImageClassifierTests
|
||||||
|
|
||||||
|
#pragma mark Results
|
||||||
|
|
||||||
|
+ (NSArray<MPPCategory *> *)expectedResultCategoriesForClassifyBurgerImageWithFloatModel {
|
||||||
|
return @[
|
||||||
|
[[MPPCategory alloc] initWithIndex:934
|
||||||
|
score:0.786005f
|
||||||
|
categoryName:@"cheeseburger"
|
||||||
|
displayName:nil],
|
||||||
|
[[MPPCategory alloc] initWithIndex:932 score:0.023508f categoryName:@"bagel" displayName:nil],
|
||||||
|
[[MPPCategory alloc] initWithIndex:925
|
||||||
|
score:0.021172f
|
||||||
|
categoryName:@"guacamole"
|
||||||
|
displayName:nil]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark File
|
||||||
|
|
||||||
|
- (NSString *)filePathWithName:(NSString *)fileName extension:(NSString *)extension {
|
||||||
|
NSString *filePath = [[NSBundle bundleForClass:self.class] pathForResource:fileName
|
||||||
|
ofType:extension];
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark Classifier Initializers
|
||||||
|
|
||||||
|
- (MPPImageClassifierOptions *)imageClassifierOptionsWithModelName:(NSString *)modelName {
|
||||||
|
NSString *modelPath = [self filePathWithName:modelName extension:@"tflite"];
|
||||||
|
MPPImageClassifierOptions *imageClassifierOptions = [[MPPImageClassifierOptions alloc] init];
|
||||||
|
imageClassifierOptions.baseOptions.modelAssetPath = modelPath;
|
||||||
|
|
||||||
|
return imageClassifierOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (MPPImageClassifier *)imageClassifierFromModelFileWithName:(NSString *)modelName {
|
||||||
|
NSString *modelPath = [self filePathWithName:modelName extension:@"tflite"];
|
||||||
|
MPPImageClassifier *imageClassifier = [[MPPImageClassifier alloc] initWithModelPath:modelPath
|
||||||
|
error:nil];
|
||||||
|
XCTAssertNotNil(imageClassifier);
|
||||||
|
|
||||||
|
return imageClassifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (MPPImageClassifier *)imageClassifierWithOptionsSucceeds:
|
||||||
|
(MPPImageClassifierOptions *)imageClassifierOptions {
|
||||||
|
MPPImageClassifier *imageClassifier =
|
||||||
|
[[MPPImageClassifier alloc] initWithOptions:imageClassifierOptions error:nil];
|
||||||
|
XCTAssertNotNil(imageClassifier);
|
||||||
|
|
||||||
|
return imageClassifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark Assert Classify Results
|
||||||
|
|
||||||
|
- (MPPImage *)imageWithFileInfo:(NSDictionary *)fileInfo {
|
||||||
|
MPPImage *image = [MPPImage imageFromBundleWithClass:[MPPImageClassifierTests class]
|
||||||
|
fileName:fileInfo[@"name"]
|
||||||
|
ofType:fileInfo[@"type"]];
|
||||||
|
XCTAssertNotNil(image);
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (MPPImage *)imageWithFileInfo:(NSDictionary *)fileInfo
|
||||||
|
orientation:(UIImageOrientation)orientation {
|
||||||
|
MPPImage *image = [MPPImage imageFromBundleWithClass:[MPPImageClassifierTests class]
|
||||||
|
fileName:fileInfo[@"name"]
|
||||||
|
ofType:fileInfo[@"type"]
|
||||||
|
orientation:orientation];
|
||||||
|
XCTAssertNotNil(image);
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)assertCreateImageClassifierWithOptions:(MPPImageClassifierOptions *)imageClassifierOptions
|
||||||
|
failsWithExpectedError:(NSError *)expectedError {
|
||||||
|
NSError *error = nil;
|
||||||
|
MPPImageClassifier *imageClassifier =
|
||||||
|
[[MPPImageClassifier alloc] initWithOptions:imageClassifierOptions error:&error];
|
||||||
|
|
||||||
|
XCTAssertNil(imageClassifier);
|
||||||
|
AssertEqualErrors(error, expectedError);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)assertImageClassifierResult:(MPPImageClassifierResult *)imageClassifierResult
|
||||||
|
hasExpectedCategoriesCount:(NSInteger)expectedCategoriesCount
|
||||||
|
expectedCategories:(NSArray<MPPCategory *> *)expectedCategories {
|
||||||
|
AssertImageClassifierResultHasOneHead(imageClassifierResult);
|
||||||
|
|
||||||
|
NSArray<MPPCategory *> *resultCategories =
|
||||||
|
imageClassifierResult.classificationResult.classifications[0].categories;
|
||||||
|
XCTAssertEqual(resultCategories.count, expectedCategoriesCount);
|
||||||
|
|
||||||
|
NSArray<MPPCategory *> *categorySubsetToCompare;
|
||||||
|
if (resultCategories.count > expectedCategories.count) {
|
||||||
|
categorySubsetToCompare =
|
||||||
|
[resultCategories subarrayWithRange:NSMakeRange(0, expectedCategories.count)];
|
||||||
|
} else {
|
||||||
|
categorySubsetToCompare = resultCategories;
|
||||||
|
}
|
||||||
|
AssertEqualCategoryArrays(categorySubsetToCompare, expectedCategories);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)assertResultsOfClassifyImage:(MPPImage *)mppImage
|
||||||
|
usingImageClassifier:(MPPImageClassifier *)imageClassifier
|
||||||
|
expectedCategoriesCount:(NSInteger)expectedCategoriesCount
|
||||||
|
equalsCategories:(NSArray<MPPCategory *> *)expectedCategories {
|
||||||
|
MPPImageClassifierResult *imageClassifierResult = [imageClassifier classifyImage:mppImage
|
||||||
|
error:nil];
|
||||||
|
|
||||||
|
[self assertImageClassifierResult:imageClassifierResult
|
||||||
|
hasExpectedCategoriesCount:expectedCategoriesCount
|
||||||
|
expectedCategories:expectedCategories];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)assertResultsOfClassifyImageWithFileInfo:(NSDictionary *)fileInfo
|
||||||
|
usingImageClassifier:(MPPImageClassifier *)imageClassifier
|
||||||
|
expectedCategoriesCount:(NSInteger)expectedCategoriesCount
|
||||||
|
equalsCategories:(NSArray<MPPCategory *> *)expectedCategories {
|
||||||
|
MPPImage *mppImage = [self imageWithFileInfo:fileInfo];
|
||||||
|
|
||||||
|
[self assertResultsOfClassifyImage:mppImage
|
||||||
|
usingImageClassifier:imageClassifier
|
||||||
|
expectedCategoriesCount:expectedCategoriesCount
|
||||||
|
equalsCategories:expectedCategories];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark General Tests
|
||||||
|
|
||||||
|
- (void)testCreateImageClassifierWithMissingModelPathFails {
|
||||||
|
NSString *modelPath = [self filePathWithName:@"" extension:@""];
|
||||||
|
|
||||||
|
NSError *error = nil;
|
||||||
|
MPPImageClassifier *imageClassifier = [[MPPImageClassifier alloc] initWithModelPath:modelPath
|
||||||
|
error:&error];
|
||||||
|
XCTAssertNil(imageClassifier);
|
||||||
|
|
||||||
|
NSError *expectedError = [NSError
|
||||||
|
errorWithDomain:kExpectedErrorDomain
|
||||||
|
code:MPPTasksErrorCodeInvalidArgumentError
|
||||||
|
userInfo:@{
|
||||||
|
NSLocalizedDescriptionKey :
|
||||||
|
@"INVALID_ARGUMENT: ExternalFile must specify at least one of 'file_content', "
|
||||||
|
@"'file_name', 'file_pointer_meta' or 'file_descriptor_meta'."
|
||||||
|
}];
|
||||||
|
AssertEqualErrors(error, expectedError);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testCreateImageClassifierAllowlistAndDenylistFails {
|
||||||
|
MPPImageClassifierOptions *options = [self imageClassifierOptionsWithModelName:kFloatModelName];
|
||||||
|
options.categoryAllowlist = @[ @"cheeseburger" ];
|
||||||
|
options.categoryDenylist = @[ @"bagel" ];
|
||||||
|
|
||||||
|
[self assertCreateImageClassifierWithOptions:options
|
||||||
|
failsWithExpectedError:
|
||||||
|
[NSError
|
||||||
|
errorWithDomain:kExpectedErrorDomain
|
||||||
|
code:MPPTasksErrorCodeInvalidArgumentError
|
||||||
|
userInfo:@{
|
||||||
|
NSLocalizedDescriptionKey :
|
||||||
|
@"INVALID_ARGUMENT: `category_allowlist` and "
|
||||||
|
@"`category_denylist` are mutually exclusive options."
|
||||||
|
}]];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testClassifyWithModelPathAndFloatModelSucceeds {
|
||||||
|
MPPImageClassifier *imageClassifier = [self imageClassifierFromModelFileWithName:kFloatModelName];
|
||||||
|
|
||||||
|
[self
|
||||||
|
assertResultsOfClassifyImageWithFileInfo:kBurgerImage
|
||||||
|
usingImageClassifier:imageClassifier
|
||||||
|
expectedCategoriesCount:kMobileNetCategoriesCount
|
||||||
|
equalsCategories:
|
||||||
|
[MPPImageClassifierTests
|
||||||
|
expectedResultCategoriesForClassifyBurgerImageWithFloatModel]];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testClassifyWithOptionsAndFloatModelSucceeds {
|
||||||
|
MPPImageClassifierOptions *options = [self imageClassifierOptionsWithModelName:kFloatModelName];
|
||||||
|
|
||||||
|
const NSInteger maxResults = 3;
|
||||||
|
options.maxResults = maxResults;
|
||||||
|
|
||||||
|
MPPImageClassifier *imageClassifier = [self imageClassifierWithOptionsSucceeds:options];
|
||||||
|
|
||||||
|
[self
|
||||||
|
assertResultsOfClassifyImageWithFileInfo:kBurgerImage
|
||||||
|
usingImageClassifier:imageClassifier
|
||||||
|
expectedCategoriesCount:maxResults
|
||||||
|
equalsCategories:
|
||||||
|
[MPPImageClassifierTests
|
||||||
|
expectedResultCategoriesForClassifyBurgerImageWithFloatModel]];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testClassifyWithQuantizedModelSucceeds {
|
||||||
|
MPPImageClassifierOptions *options =
|
||||||
|
[self imageClassifierOptionsWithModelName:kQuantizedModelName];
|
||||||
|
|
||||||
|
const NSInteger maxResults = 1;
|
||||||
|
options.maxResults = maxResults;
|
||||||
|
|
||||||
|
MPPImageClassifier *imageClassifier = [self imageClassifierWithOptionsSucceeds:options];
|
||||||
|
|
||||||
|
NSArray<MPPCategory *> *expectedCategories = @[ [[MPPCategory alloc] initWithIndex:934
|
||||||
|
score:0.972656f
|
||||||
|
categoryName:@"cheeseburger"
|
||||||
|
displayName:nil] ];
|
||||||
|
|
||||||
|
[self assertResultsOfClassifyImageWithFileInfo:kBurgerImage
|
||||||
|
usingImageClassifier:imageClassifier
|
||||||
|
expectedCategoriesCount:maxResults
|
||||||
|
equalsCategories:expectedCategories];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testClassifyWithScoreThresholdSucceeds {
|
||||||
|
MPPImageClassifierOptions *options = [self imageClassifierOptionsWithModelName:kFloatModelName];
|
||||||
|
|
||||||
|
options.scoreThreshold = 0.25f;
|
||||||
|
|
||||||
|
MPPImageClassifier *imageClassifier = [self imageClassifierWithOptionsSucceeds:options];
|
||||||
|
|
||||||
|
NSArray<MPPCategory *> *expectedCategories = @[ [[MPPCategory alloc] initWithIndex:934
|
||||||
|
score:0.786005f
|
||||||
|
categoryName:@"cheeseburger"
|
||||||
|
displayName:nil] ];
|
||||||
|
|
||||||
|
[self assertResultsOfClassifyImageWithFileInfo:kBurgerImage
|
||||||
|
usingImageClassifier:imageClassifier
|
||||||
|
expectedCategoriesCount:expectedCategories.count
|
||||||
|
equalsCategories:expectedCategories];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testClassifyWithAllowlistSucceeds {
|
||||||
|
MPPImageClassifierOptions *options = [self imageClassifierOptionsWithModelName:kFloatModelName];
|
||||||
|
|
||||||
|
options.categoryAllowlist = @[ @"cheeseburger", @"guacamole", @"meat loaf" ];
|
||||||
|
|
||||||
|
MPPImageClassifier *imageClassifier = [self imageClassifierWithOptionsSucceeds:options];
|
||||||
|
|
||||||
|
NSArray<MPPCategory *> *expectedCategories = @[
|
||||||
|
[[MPPCategory alloc] initWithIndex:934
|
||||||
|
score:0.786005f
|
||||||
|
categoryName:@"cheeseburger"
|
||||||
|
displayName:nil],
|
||||||
|
[[MPPCategory alloc] initWithIndex:925
|
||||||
|
score:0.021172f
|
||||||
|
categoryName:@"guacamole"
|
||||||
|
displayName:nil],
|
||||||
|
[[MPPCategory alloc] initWithIndex:963
|
||||||
|
score:0.006279315f
|
||||||
|
categoryName:@"meat loaf"
|
||||||
|
displayName:nil],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
[self assertResultsOfClassifyImageWithFileInfo:kBurgerImage
|
||||||
|
usingImageClassifier:imageClassifier
|
||||||
|
expectedCategoriesCount:expectedCategories.count
|
||||||
|
equalsCategories:expectedCategories];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testClassifyWithDenylistSucceeds {
|
||||||
|
MPPImageClassifierOptions *options = [self imageClassifierOptionsWithModelName:kFloatModelName];
|
||||||
|
|
||||||
|
options.categoryDenylist = @[
|
||||||
|
@"bagel",
|
||||||
|
];
|
||||||
|
options.maxResults = 3;
|
||||||
|
|
||||||
|
MPPImageClassifier *imageClassifier = [self imageClassifierWithOptionsSucceeds:options];
|
||||||
|
|
||||||
|
NSArray<MPPCategory *> *expectedCategories = @[
|
||||||
|
[[MPPCategory alloc] initWithIndex:934
|
||||||
|
score:0.786005f
|
||||||
|
categoryName:@"cheeseburger"
|
||||||
|
displayName:nil],
|
||||||
|
[[MPPCategory alloc] initWithIndex:925
|
||||||
|
score:0.021172f
|
||||||
|
categoryName:@"guacamole"
|
||||||
|
displayName:nil],
|
||||||
|
[[MPPCategory alloc] initWithIndex:963
|
||||||
|
score:0.006279315f
|
||||||
|
categoryName:@"meat loaf"
|
||||||
|
displayName:nil],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
[self assertResultsOfClassifyImageWithFileInfo:kBurgerImage
|
||||||
|
usingImageClassifier:imageClassifier
|
||||||
|
expectedCategoriesCount:expectedCategories.count
|
||||||
|
equalsCategories:expectedCategories];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testClassifyWithRegionOfInterestSucceeds {
|
||||||
|
MPPImageClassifierOptions *options = [self imageClassifierOptionsWithModelName:kFloatModelName];
|
||||||
|
|
||||||
|
NSInteger maxResults = 1;
|
||||||
|
options.maxResults = maxResults;
|
||||||
|
|
||||||
|
MPPImageClassifier *imageClassifier = [self imageClassifierWithOptionsSucceeds:options];
|
||||||
|
|
||||||
|
NSArray<MPPCategory *> *expectedCategories = @[ [[MPPCategory alloc] initWithIndex:806
|
||||||
|
score:0.997122f
|
||||||
|
categoryName:@"soccer ball"
|
||||||
|
displayName:nil] ];
|
||||||
|
|
||||||
|
MPPImage *image = [self imageWithFileInfo:kMultiObjectsImage];
|
||||||
|
|
||||||
|
// roi around soccer ball
|
||||||
|
MPPImageClassifierResult *imageClassifierResult =
|
||||||
|
[imageClassifier classifyImage:image
|
||||||
|
regionOfInterest:CGRectMake(0.450f, 0.308f, 0.164f, 0.426f)
|
||||||
|
error:nil];
|
||||||
|
[self assertImageClassifierResult:imageClassifierResult
|
||||||
|
hasExpectedCategoriesCount:maxResults
|
||||||
|
expectedCategories:expectedCategories];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testClassifyWithOrientationSucceeds {
|
||||||
|
MPPImageClassifierOptions *options = [self imageClassifierOptionsWithModelName:kFloatModelName];
|
||||||
|
|
||||||
|
NSInteger maxResults = 3;
|
||||||
|
options.maxResults = maxResults;
|
||||||
|
|
||||||
|
MPPImageClassifier *imageClassifier = [self imageClassifierWithOptionsSucceeds:options];
|
||||||
|
|
||||||
|
NSArray<MPPCategory *> *expectedCategories = @[
|
||||||
|
[[MPPCategory alloc] initWithIndex:934
|
||||||
|
score:0.622074f
|
||||||
|
categoryName:@"cheeseburger"
|
||||||
|
displayName:nil],
|
||||||
|
[[MPPCategory alloc] initWithIndex:963
|
||||||
|
score:0.051214f
|
||||||
|
categoryName:@"meat loaf"
|
||||||
|
displayName:nil],
|
||||||
|
[[MPPCategory alloc] initWithIndex:925
|
||||||
|
score:0.048719f
|
||||||
|
categoryName:@"guacamole"
|
||||||
|
displayName:nil]
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
MPPImage *image = [self imageWithFileInfo:kBurgerRotatedImage
|
||||||
|
orientation:UIImageOrientationRight];
|
||||||
|
|
||||||
|
[self assertResultsOfClassifyImage:image
|
||||||
|
usingImageClassifier:imageClassifier
|
||||||
|
expectedCategoriesCount:maxResults
|
||||||
|
equalsCategories:expectedCategories];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testClassifyWithRegionOfInterestAndOrientationSucceeds {
|
||||||
|
MPPImageClassifierOptions *options = [self imageClassifierOptionsWithModelName:kFloatModelName];
|
||||||
|
|
||||||
|
NSInteger maxResults = 1;
|
||||||
|
options.maxResults = maxResults;
|
||||||
|
|
||||||
|
MPPImageClassifier *imageClassifier = [self imageClassifierWithOptionsSucceeds:options];
|
||||||
|
|
||||||
|
NSArray<MPPCategory *> *expectedCategories =
|
||||||
|
@[ [[MPPCategory alloc] initWithIndex:560
|
||||||
|
score:0.682305f
|
||||||
|
categoryName:@"folding chair"
|
||||||
|
displayName:nil] ];
|
||||||
|
|
||||||
|
MPPImage *image = [self imageWithFileInfo:kMultiObjectsRotatedImage
|
||||||
|
orientation:UIImageOrientationRight];
|
||||||
|
|
||||||
|
// roi around folding chair
|
||||||
|
MPPImageClassifierResult *imageClassifierResult =
|
||||||
|
[imageClassifier classifyImage:image
|
||||||
|
regionOfInterest:CGRectMake(0.0f, 0.1763f, 0.5642f, 0.1286f)
|
||||||
|
error:nil];
|
||||||
|
[self assertImageClassifierResult:imageClassifierResult
|
||||||
|
hasExpectedCategoriesCount:maxResults
|
||||||
|
expectedCategories:expectedCategories];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark Running Mode Tests
|
||||||
|
|
||||||
|
- (void)testCreateImageClassifierFailsWithResultListenerInNonLiveStreamMode {
|
||||||
|
MPPRunningMode runningModesToTest[] = {MPPRunningModeImage, MPPRunningModeVideo};
|
||||||
|
for (int i = 0; i < sizeof(runningModesToTest) / sizeof(runningModesToTest[0]); i++) {
|
||||||
|
MPPImageClassifierOptions *options = [self imageClassifierOptionsWithModelName:kFloatModelName];
|
||||||
|
|
||||||
|
options.runningMode = runningModesToTest[i];
|
||||||
|
options.completion = ^(MPPImageClassifierResult *result, NSError *error) {
|
||||||
|
};
|
||||||
|
|
||||||
|
[self
|
||||||
|
assertCreateImageClassifierWithOptions:options
|
||||||
|
failsWithExpectedError:
|
||||||
|
[NSError
|
||||||
|
errorWithDomain:kExpectedErrorDomain
|
||||||
|
code:MPPTasksErrorCodeInvalidArgumentError
|
||||||
|
userInfo:@{
|
||||||
|
NSLocalizedDescriptionKey :
|
||||||
|
@"The vision task is in image or video mode, a "
|
||||||
|
@"user-defined result callback should not be provided."
|
||||||
|
}]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testCreateImageClassifierFailsWithMissingResultListenerInLiveStreamMode {
|
||||||
|
MPPImageClassifierOptions *options = [self imageClassifierOptionsWithModelName:kFloatModelName];
|
||||||
|
|
||||||
|
options.runningMode = MPPRunningModeLiveStream;
|
||||||
|
|
||||||
|
[self assertCreateImageClassifierWithOptions:options
|
||||||
|
failsWithExpectedError:
|
||||||
|
[NSError errorWithDomain:kExpectedErrorDomain
|
||||||
|
code:MPPTasksErrorCodeInvalidArgumentError
|
||||||
|
userInfo:@{
|
||||||
|
NSLocalizedDescriptionKey :
|
||||||
|
@"The vision task is in live stream mode, a "
|
||||||
|
@"user-defined result callback must be provided."
|
||||||
|
}]];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testClassifyFailsWithCallingWrongApiInImageMode {
|
||||||
|
MPPImageClassifierOptions *options = [self imageClassifierOptionsWithModelName:kFloatModelName];
|
||||||
|
|
||||||
|
MPPImageClassifier *imageClassifier = [self imageClassifierWithOptionsSucceeds:options];
|
||||||
|
|
||||||
|
MPPImage *image = [self imageWithFileInfo:kBurgerImage];
|
||||||
|
|
||||||
|
NSError *liveStreamApiCallError;
|
||||||
|
XCTAssertFalse([imageClassifier classifyAsyncImage:image
|
||||||
|
timestampMs:0
|
||||||
|
error:&liveStreamApiCallError]);
|
||||||
|
|
||||||
|
NSError *expectedLiveStreamApiCallError =
|
||||||
|
[NSError errorWithDomain:kExpectedErrorDomain
|
||||||
|
code:MPPTasksErrorCodeInvalidArgumentError
|
||||||
|
userInfo:@{
|
||||||
|
NSLocalizedDescriptionKey : @"The vision task is not initialized with live "
|
||||||
|
@"stream mode. Current Running Mode: Image"
|
||||||
|
}];
|
||||||
|
|
||||||
|
AssertEqualErrors(liveStreamApiCallError, expectedLiveStreamApiCallError);
|
||||||
|
|
||||||
|
NSError *videoApiCallError;
|
||||||
|
XCTAssertFalse([imageClassifier classifyVideoFrame:image timestampMs:0 error:&videoApiCallError]);
|
||||||
|
|
||||||
|
NSError *expectedVideoApiCallError =
|
||||||
|
[NSError errorWithDomain:kExpectedErrorDomain
|
||||||
|
code:MPPTasksErrorCodeInvalidArgumentError
|
||||||
|
userInfo:@{
|
||||||
|
NSLocalizedDescriptionKey : @"The vision task is not initialized with "
|
||||||
|
@"video mode. Current Running Mode: Image"
|
||||||
|
}];
|
||||||
|
AssertEqualErrors(videoApiCallError, expectedVideoApiCallError);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testClassifyFailsWithCallingWrongApiInVideoMode {
|
||||||
|
MPPImageClassifierOptions *options = [self imageClassifierOptionsWithModelName:kFloatModelName];
|
||||||
|
|
||||||
|
options.runningMode = MPPRunningModeVideo;
|
||||||
|
|
||||||
|
MPPImageClassifier *imageClassifier = [self imageClassifierWithOptionsSucceeds:options];
|
||||||
|
|
||||||
|
MPPImage *image = [self imageWithFileInfo:kBurgerImage];
|
||||||
|
|
||||||
|
NSError *liveStreamApiCallError;
|
||||||
|
XCTAssertFalse([imageClassifier classifyAsyncImage:image
|
||||||
|
timestampMs:0
|
||||||
|
error:&liveStreamApiCallError]);
|
||||||
|
|
||||||
|
NSError *expectedLiveStreamApiCallError =
|
||||||
|
[NSError errorWithDomain:kExpectedErrorDomain
|
||||||
|
code:MPPTasksErrorCodeInvalidArgumentError
|
||||||
|
userInfo:@{
|
||||||
|
NSLocalizedDescriptionKey : @"The vision task is not initialized with live "
|
||||||
|
@"stream mode. Current Running Mode: Video"
|
||||||
|
}];
|
||||||
|
|
||||||
|
AssertEqualErrors(liveStreamApiCallError, expectedLiveStreamApiCallError);
|
||||||
|
|
||||||
|
NSError *imageApiCallError;
|
||||||
|
XCTAssertFalse([imageClassifier classifyImage:image error:&imageApiCallError]);
|
||||||
|
|
||||||
|
NSError *expectedImageApiCallError =
|
||||||
|
[NSError errorWithDomain:kExpectedErrorDomain
|
||||||
|
code:MPPTasksErrorCodeInvalidArgumentError
|
||||||
|
userInfo:@{
|
||||||
|
NSLocalizedDescriptionKey : @"The vision task is not initialized with "
|
||||||
|
@"image mode. Current Running Mode: Video"
|
||||||
|
}];
|
||||||
|
AssertEqualErrors(imageApiCallError, expectedImageApiCallError);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testClassifyFailsWithCallingWrongApiInLiveStreamMode {
|
||||||
|
MPPImageClassifierOptions *options = [self imageClassifierOptionsWithModelName:kFloatModelName];
|
||||||
|
|
||||||
|
options.runningMode = MPPRunningModeLiveStream;
|
||||||
|
options.completion = ^(MPPImageClassifierResult *result, NSError *error) {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
MPPImageClassifier *imageClassifier = [self imageClassifierWithOptionsSucceeds:options];
|
||||||
|
|
||||||
|
MPPImage *image = [self imageWithFileInfo:kBurgerImage];
|
||||||
|
|
||||||
|
NSError *imageApiCallError;
|
||||||
|
XCTAssertFalse([imageClassifier classifyImage:image error:&imageApiCallError]);
|
||||||
|
|
||||||
|
NSError *expectedImageApiCallError =
|
||||||
|
[NSError errorWithDomain:kExpectedErrorDomain
|
||||||
|
code:MPPTasksErrorCodeInvalidArgumentError
|
||||||
|
userInfo:@{
|
||||||
|
NSLocalizedDescriptionKey : @"The vision task is not initialized with "
|
||||||
|
@"image mode. Current Running Mode: Live Stream"
|
||||||
|
}];
|
||||||
|
AssertEqualErrors(imageApiCallError, expectedImageApiCallError);
|
||||||
|
|
||||||
|
NSError *videoApiCallError;
|
||||||
|
XCTAssertFalse([imageClassifier classifyVideoFrame:image timestampMs:0 error:&videoApiCallError]);
|
||||||
|
|
||||||
|
NSError *expectedVideoApiCallError =
|
||||||
|
[NSError errorWithDomain:kExpectedErrorDomain
|
||||||
|
code:MPPTasksErrorCodeInvalidArgumentError
|
||||||
|
userInfo:@{
|
||||||
|
NSLocalizedDescriptionKey : @"The vision task is not initialized with "
|
||||||
|
@"video mode. Current Running Mode: Live Stream"
|
||||||
|
}];
|
||||||
|
AssertEqualErrors(videoApiCallError, expectedVideoApiCallError);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testClassifyWithVideoModeSucceeds {
|
||||||
|
MPPImageClassifierOptions *options = [self imageClassifierOptionsWithModelName:kFloatModelName];
|
||||||
|
|
||||||
|
options.runningMode = MPPRunningModeVideo;
|
||||||
|
|
||||||
|
NSInteger maxResults = 3;
|
||||||
|
options.maxResults = maxResults;
|
||||||
|
|
||||||
|
MPPImageClassifier *imageClassifier = [self imageClassifierWithOptionsSucceeds:options];
|
||||||
|
|
||||||
|
MPPImage *image = [self imageWithFileInfo:kBurgerImage];
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
MPPImageClassifierResult *imageClassifierResult = [imageClassifier classifyVideoFrame:image
|
||||||
|
timestampMs:i
|
||||||
|
error:nil];
|
||||||
|
[self assertImageClassifierResult:imageClassifierResult
|
||||||
|
hasExpectedCategoriesCount:maxResults
|
||||||
|
expectedCategories:
|
||||||
|
[MPPImageClassifierTests
|
||||||
|
expectedResultCategoriesForClassifyBurgerImageWithFloatModel]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testClassifyWithOutOfOrderTimestampsAndLiveStreamModeFails {
|
||||||
|
MPPImageClassifierOptions *options = [self imageClassifierOptionsWithModelName:kFloatModelName];
|
||||||
|
|
||||||
|
NSInteger maxResults = 3;
|
||||||
|
options.maxResults = maxResults;
|
||||||
|
|
||||||
|
options.runningMode = MPPRunningModeLiveStream;
|
||||||
|
options.completion = ^(MPPImageClassifierResult *result, NSError *error) {
|
||||||
|
[self assertImageClassifierResult:result
|
||||||
|
hasExpectedCategoriesCount:maxResults
|
||||||
|
expectedCategories:
|
||||||
|
[MPPImageClassifierTests
|
||||||
|
expectedResultCategoriesForClassifyBurgerImageWithFloatModel]];
|
||||||
|
};
|
||||||
|
|
||||||
|
MPPImageClassifier *imageClassifier = [self imageClassifierWithOptionsSucceeds:options];
|
||||||
|
|
||||||
|
MPPImage *image = [self imageWithFileInfo:kBurgerImage];
|
||||||
|
|
||||||
|
XCTAssertTrue([imageClassifier classifyAsyncImage:image timestampMs:1 error:nil]);
|
||||||
|
|
||||||
|
NSError *error;
|
||||||
|
XCTAssertFalse([imageClassifier classifyAsyncImage:image timestampMs:0 error:&error]);
|
||||||
|
|
||||||
|
NSError *expectedError =
|
||||||
|
[NSError errorWithDomain:kExpectedErrorDomain
|
||||||
|
code:MPPTasksErrorCodeInvalidArgumentError
|
||||||
|
userInfo:@{
|
||||||
|
NSLocalizedDescriptionKey :
|
||||||
|
@"INVALID_ARGUMENT: Input timestamp must be monotonically increasing."
|
||||||
|
}];
|
||||||
|
AssertEqualErrors(error, expectedError);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testClassifyWithLiveStreamModeSucceeds {
|
||||||
|
MPPImageClassifierOptions *options = [self imageClassifierOptionsWithModelName:kFloatModelName];
|
||||||
|
|
||||||
|
NSInteger maxResults = 3;
|
||||||
|
options.maxResults = maxResults;
|
||||||
|
|
||||||
|
options.runningMode = MPPRunningModeLiveStream;
|
||||||
|
options.completion = ^(MPPImageClassifierResult *result, NSError *error) {
|
||||||
|
[self assertImageClassifierResult:result
|
||||||
|
hasExpectedCategoriesCount:maxResults
|
||||||
|
expectedCategories:
|
||||||
|
[MPPImageClassifierTests
|
||||||
|
expectedResultCategoriesForClassifyBurgerImageWithFloatModel]];
|
||||||
|
};
|
||||||
|
|
||||||
|
MPPImageClassifier *imageClassifier = [self imageClassifierWithOptionsSucceeds:options];
|
||||||
|
|
||||||
|
// TODO: Mimic initialization from CMSampleBuffer as live stream mode is most likely to be used
|
||||||
|
// with the iOS camera. AVCaptureVideoDataOutput sample buffer delegates provide frames of type
|
||||||
|
// `CMSampleBuffer`.
|
||||||
|
MPPImage *image = [self imageWithFileInfo:kBurgerImage];
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
XCTAssertTrue([imageClassifier classifyAsyncImage:image timestampMs:i error:nil]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
13
mediapipe/tasks/ios/test/vision/utils/BUILD
Normal file
13
mediapipe/tasks/ios/test/vision/utils/BUILD
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package(default_visibility = ["//mediapipe/tasks:internal"])
|
||||||
|
|
||||||
|
licenses(["notice"])
|
||||||
|
|
||||||
|
objc_library(
|
||||||
|
name = "MPPImageTestUtils",
|
||||||
|
srcs = ["sources/MPPImage+TestUtils.m"],
|
||||||
|
hdrs = ["sources/MPPImage+TestUtils.h"],
|
||||||
|
module_name = "MPPImageTestUtils",
|
||||||
|
deps = [
|
||||||
|
"//mediapipe/tasks/ios/vision/core:MPPImage",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,63 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
#import "mediapipe/tasks/ios/vision/core/sources/MPPImage.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper utility for initializing `MPPImage` for MediaPipe iOS vision library tests.
|
||||||
|
*/
|
||||||
|
@interface MPPImage (TestUtils)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads an image from a file in an app bundle into a `MPPImage` object.
|
||||||
|
*
|
||||||
|
* @param classObject The specified class associated with the bundle containing the file to be
|
||||||
|
* loaded.
|
||||||
|
* @param name Name of the image file.
|
||||||
|
* @param type Extenstion of the image file.
|
||||||
|
*
|
||||||
|
* @return The `MPPImage` object contains the loaded image. This method returns
|
||||||
|
* nil if it cannot load the image.
|
||||||
|
*/
|
||||||
|
+ (nullable MPPImage *)imageFromBundleWithClass:(Class)classObject
|
||||||
|
fileName:(NSString *)name
|
||||||
|
ofType:(NSString *)type
|
||||||
|
NS_SWIFT_NAME(imageFromBundle(class:filename:type:));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads an image from a file in an app bundle into a `MPPImage` object with the specified
|
||||||
|
* orientation.
|
||||||
|
*
|
||||||
|
* @param classObject The specified class associated with the bundle containing the file to be
|
||||||
|
* loaded.
|
||||||
|
* @param name Name of the image file.
|
||||||
|
* @param type Extenstion of the image file.
|
||||||
|
* @param orientation Orientation of the image.
|
||||||
|
*
|
||||||
|
* @return The `MPPImage` object contains the loaded image. This method returns
|
||||||
|
* nil if it cannot load the image.
|
||||||
|
*/
|
||||||
|
+ (nullable MPPImage *)imageFromBundleWithClass:(Class)classObject
|
||||||
|
fileName:(NSString *)name
|
||||||
|
ofType:(NSString *)type
|
||||||
|
orientation:(UIImageOrientation)imageOrientation
|
||||||
|
NS_SWIFT_NAME(imageFromBundle(class:filename:type:orientation:));
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,57 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#import "mediapipe/tasks/ios/test/vision/utils/sources/MPPImage+TestUtils.h"
|
||||||
|
|
||||||
|
@interface UIImage (FileUtils)
|
||||||
|
|
||||||
|
+ (nullable UIImage *)imageFromBundleWithClass:(Class)classObject
|
||||||
|
fileName:(NSString *)name
|
||||||
|
ofType:(NSString *)type;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation UIImage (FileUtils)
|
||||||
|
|
||||||
|
+ (nullable UIImage *)imageFromBundleWithClass:(Class)classObject
|
||||||
|
fileName:(NSString *)name
|
||||||
|
ofType:(NSString *)type {
|
||||||
|
NSString *imagePath = [[NSBundle bundleForClass:classObject] pathForResource:name ofType:type];
|
||||||
|
if (!imagePath) return nil;
|
||||||
|
|
||||||
|
return [[UIImage alloc] initWithContentsOfFile:imagePath];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation MPPImage (TestUtils)
|
||||||
|
|
||||||
|
+ (nullable MPPImage *)imageFromBundleWithClass:(Class)classObject
|
||||||
|
fileName:(NSString *)name
|
||||||
|
ofType:(NSString *)type {
|
||||||
|
UIImage *image = [UIImage imageFromBundleWithClass:classObject fileName:name ofType:type];
|
||||||
|
|
||||||
|
return [[MPPImage alloc] initWithUIImage:image error:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (nullable MPPImage *)imageFromBundleWithClass:(Class)classObject
|
||||||
|
fileName:(NSString *)name
|
||||||
|
ofType:(NSString *)type
|
||||||
|
orientation:(UIImageOrientation)imageOrientation {
|
||||||
|
UIImage *image = [UIImage imageFromBundleWithClass:classObject fileName:name ofType:type];
|
||||||
|
|
||||||
|
return [[MPPImage alloc] initWithUIImage:image orientation:imageOrientation error:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
|
@ -54,11 +54,13 @@ objc_library(
|
||||||
],
|
],
|
||||||
deps = [
|
deps = [
|
||||||
":MPPRunningMode",
|
":MPPRunningMode",
|
||||||
|
"//mediapipe/calculators/core:flow_limiter_calculator",
|
||||||
"//mediapipe/framework/formats:rect_cc_proto",
|
"//mediapipe/framework/formats:rect_cc_proto",
|
||||||
"//mediapipe/tasks/ios/common:MPPCommon",
|
"//mediapipe/tasks/ios/common:MPPCommon",
|
||||||
"//mediapipe/tasks/ios/common/utils:MPPCommonUtils",
|
"//mediapipe/tasks/ios/common/utils:MPPCommonUtils",
|
||||||
"//mediapipe/tasks/ios/core:MPPTaskRunner",
|
"//mediapipe/tasks/ios/core:MPPTaskRunner",
|
||||||
"//third_party/apple_frameworks:UIKit",
|
"//third_party/apple_frameworks:UIKit",
|
||||||
"@com_google_absl//absl/status:statusor",
|
"@com_google_absl//absl/status:statusor",
|
||||||
|
"@ios_opencv//:OpencvFramework",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -44,9 +44,9 @@ NS_INLINE NSString *MPPRunningModeDisplayName(MPPRunningMode runningMode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString *displayNameMap[MPPRunningModeLiveStream + 1] = {
|
NSString *displayNameMap[MPPRunningModeLiveStream + 1] = {
|
||||||
[MPPRunningModeImage] = @"#MPPRunningModeImage",
|
[MPPRunningModeImage] = @"Image",
|
||||||
[MPPRunningModeVideo] = @ "#MPPRunningModeVideo",
|
[MPPRunningModeVideo] = @"Video",
|
||||||
[MPPRunningModeLiveStream] = @ "#MPPRunningModeLiveStream"};
|
[MPPRunningModeLiveStream] = @"Live Stream"};
|
||||||
|
|
||||||
return displayNameMap[runningMode];
|
return displayNameMap[runningMode];
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,7 +97,7 @@ static const NSInteger kMPPOrientationDegreesLeft = -270;
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
CGRect calculatedRoi = CGRectEqualToRect(roi, CGRectZero) ? roi : CGRectMake(0.0, 0.0, 1.0, 1.0);
|
CGRect calculatedRoi = CGRectEqualToRect(roi, CGRectZero) ? CGRectMake(0.0, 0.0, 1.0, 1.0) : roi;
|
||||||
|
|
||||||
NormalizedRect normalizedRect;
|
NormalizedRect normalizedRect;
|
||||||
normalizedRect.set_x_center(CGRectGetMidX(calculatedRoi));
|
normalizedRect.set_x_center(CGRectGetMidX(calculatedRoi));
|
||||||
|
|
|
@ -131,7 +131,9 @@ using ::mediapipe::ImageFrame;
|
||||||
|
|
||||||
size_t width = CVPixelBufferGetWidth(pixelBuffer);
|
size_t width = CVPixelBufferGetWidth(pixelBuffer);
|
||||||
size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
||||||
size_t stride = CVPixelBufferGetBytesPerRow(pixelBuffer);
|
|
||||||
|
size_t destinationChannelCount = 3;
|
||||||
|
size_t destinationStride = destinationChannelCount * width;
|
||||||
|
|
||||||
uint8_t *rgbPixelData = [MPPPixelDataUtils
|
uint8_t *rgbPixelData = [MPPPixelDataUtils
|
||||||
rgbPixelDataFromPixelData:(uint8_t *)CVPixelBufferGetBaseAddress(pixelBuffer)
|
rgbPixelDataFromPixelData:(uint8_t *)CVPixelBufferGetBaseAddress(pixelBuffer)
|
||||||
|
@ -147,9 +149,10 @@ using ::mediapipe::ImageFrame;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<ImageFrame> imageFrame = absl::make_unique<ImageFrame>(
|
std::unique_ptr<ImageFrame> imageFrame =
|
||||||
::mediapipe::ImageFormat::SRGB, width, height, stride, static_cast<uint8 *>(rgbPixelData),
|
absl::make_unique<ImageFrame>(::mediapipe::ImageFormat::SRGB, width, height,
|
||||||
/*deleter=*/free);
|
destinationStride, static_cast<uint8 *>(rgbPixelData),
|
||||||
|
/*deleter=*/free);
|
||||||
|
|
||||||
return imageFrame;
|
return imageFrame;
|
||||||
}
|
}
|
||||||
|
@ -183,11 +186,14 @@ using ::mediapipe::ImageFrame;
|
||||||
|
|
||||||
NSInteger bitsPerComponent = 8;
|
NSInteger bitsPerComponent = 8;
|
||||||
NSInteger channelCount = 4;
|
NSInteger channelCount = 4;
|
||||||
|
size_t bytesPerRow = channelCount * width;
|
||||||
|
|
||||||
|
NSInteger destinationChannelCount = 3;
|
||||||
|
size_t destinationBytesPerRow = destinationChannelCount * width;
|
||||||
|
|
||||||
UInt8 *pixelDataToReturn = NULL;
|
UInt8 *pixelDataToReturn = NULL;
|
||||||
|
|
||||||
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
||||||
size_t bytesPerRow = channelCount * width;
|
|
||||||
|
|
||||||
// iOS infers bytesPerRow if it is set to 0.
|
// iOS infers bytesPerRow if it is set to 0.
|
||||||
// See https://developer.apple.com/documentation/coregraphics/1455939-cgbitmapcontextcreate
|
// See https://developer.apple.com/documentation/coregraphics/1455939-cgbitmapcontextcreate
|
||||||
// But for segmentation test image, this was not the case.
|
// But for segmentation test image, this was not the case.
|
||||||
|
@ -219,10 +225,14 @@ using ::mediapipe::ImageFrame;
|
||||||
|
|
||||||
CGColorSpaceRelease(colorSpace);
|
CGColorSpaceRelease(colorSpace);
|
||||||
|
|
||||||
std::unique_ptr<ImageFrame> imageFrame =
|
if (!pixelDataToReturn) {
|
||||||
absl::make_unique<ImageFrame>(mediapipe::ImageFormat::SRGB, (int)width, (int)height,
|
return nullptr;
|
||||||
(int)bytesPerRow, static_cast<uint8 *>(pixelDataToReturn),
|
}
|
||||||
/*deleter=*/free);
|
|
||||||
|
std::unique_ptr<ImageFrame> imageFrame = absl::make_unique<ImageFrame>(
|
||||||
|
mediapipe::ImageFormat::SRGB, (int)width, (int)height, (int)destinationBytesPerRow,
|
||||||
|
static_cast<uint8 *>(pixelDataToReturn),
|
||||||
|
/*deleter=*/free);
|
||||||
|
|
||||||
return imageFrame;
|
return imageFrame;
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,15 +36,17 @@ static NSString *const kClassificationsTag = @"CLASSIFICATIONS";
|
||||||
static NSString *const kImageInStreamName = @"image_in";
|
static NSString *const kImageInStreamName = @"image_in";
|
||||||
static NSString *const kImageOutStreamName = @"image_out";
|
static NSString *const kImageOutStreamName = @"image_out";
|
||||||
static NSString *const kImageTag = @"IMAGE";
|
static NSString *const kImageTag = @"IMAGE";
|
||||||
static NSString *const kNormRectName = @"norm_rect_in";
|
static NSString *const kNormRectStreamName = @"norm_rect_in";
|
||||||
static NSString *const kNormRectTag = @"NORM_RECT";
|
static NSString *const kNormRectTag = @"NORM_RECT";
|
||||||
|
|
||||||
static NSString *const kTaskGraphName =
|
static NSString *const kTaskGraphName =
|
||||||
@"mediapipe.tasks.vision.image_classifier.ImageClassifierGraph";
|
@"mediapipe.tasks.vision.image_classifier.ImageClassifierGraph";
|
||||||
|
|
||||||
#define InputPacketMap(imagePacket, normalizedRectPacket) \
|
#define InputPacketMap(imagePacket, normalizedRectPacket) \
|
||||||
{ \
|
{ \
|
||||||
{kImageInStreamName.cppString, imagePacket}, { kNormRectName.cppString, normalizedRectPacket } \
|
{kImageInStreamName.cppString, imagePacket}, { \
|
||||||
|
kNormRectStreamName.cppString, normalizedRectPacket \
|
||||||
|
} \
|
||||||
}
|
}
|
||||||
|
|
||||||
@interface MPPImageClassifier () {
|
@interface MPPImageClassifier () {
|
||||||
|
@ -60,12 +62,17 @@ static NSString *const kTaskGraphName =
|
||||||
if (self) {
|
if (self) {
|
||||||
MPPTaskInfo *taskInfo = [[MPPTaskInfo alloc]
|
MPPTaskInfo *taskInfo = [[MPPTaskInfo alloc]
|
||||||
initWithTaskGraphName:kTaskGraphName
|
initWithTaskGraphName:kTaskGraphName
|
||||||
inputStreams:@[ [NSString
|
inputStreams:@[
|
||||||
stringWithFormat:@"%@:%@", kImageTag, kImageInStreamName] ]
|
[NSString stringWithFormat:@"%@:%@", kImageTag, kImageInStreamName],
|
||||||
outputStreams:@[ [NSString stringWithFormat:@"%@:%@", kClassificationsTag,
|
[NSString stringWithFormat:@"%@:%@", kNormRectTag, kNormRectStreamName]
|
||||||
kClassificationsStreamName] ]
|
]
|
||||||
|
outputStreams:@[
|
||||||
|
[NSString
|
||||||
|
stringWithFormat:@"%@:%@", kClassificationsTag, kClassificationsStreamName],
|
||||||
|
[NSString stringWithFormat:@"%@:%@", kImageTag, kImageOutStreamName]
|
||||||
|
]
|
||||||
taskOptions:options
|
taskOptions:options
|
||||||
enableFlowLimiting:NO
|
enableFlowLimiting:options.runningMode == MPPRunningModeLiveStream
|
||||||
error:error];
|
error:error];
|
||||||
|
|
||||||
if (!taskInfo) {
|
if (!taskInfo) {
|
||||||
|
@ -130,8 +137,8 @@ static NSString *const kTaskGraphName =
|
||||||
|
|
||||||
PacketMap inputPacketMap = InputPacketMap(imagePacket, normalizedRectPacket);
|
PacketMap inputPacketMap = InputPacketMap(imagePacket, normalizedRectPacket);
|
||||||
|
|
||||||
std::optional<PacketMap> outputPacketMap = [_visionTaskRunner processPacketMap:inputPacketMap
|
std::optional<PacketMap> outputPacketMap = [_visionTaskRunner processImagePacketMap:inputPacketMap
|
||||||
error:error];
|
error:error];
|
||||||
if (!outputPacketMap.has_value()) {
|
if (!outputPacketMap.has_value()) {
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
#include "mediapipe/tasks/cc/vision/image_classifier/proto/image_classifier_graph_options.pb.h"
|
#include "mediapipe/tasks/cc/vision/image_classifier/proto/image_classifier_graph_options.pb.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
using CalculatorOptionsProto = ::mediapipe::CalculatorOptions;
|
using CalculatorOptionsProto = mediapipe::CalculatorOptions;
|
||||||
using ImageClassifierGraphOptionsProto =
|
using ImageClassifierGraphOptionsProto =
|
||||||
::mediapipe::tasks::vision::image_classifier::proto::ImageClassifierGraphOptions;
|
::mediapipe::tasks::vision::image_classifier::proto::ImageClassifierGraphOptions;
|
||||||
using ClassifierOptionsProto = ::mediapipe::tasks::components::processors::proto::ClassifierOptions;
|
using ClassifierOptionsProto = ::mediapipe::tasks::components::processors::proto::ClassifierOptions;
|
||||||
|
@ -32,7 +32,9 @@ using ClassifierOptionsProto = ::mediapipe::tasks::components::processors::proto
|
||||||
- (void)copyToProto:(CalculatorOptionsProto *)optionsProto {
|
- (void)copyToProto:(CalculatorOptionsProto *)optionsProto {
|
||||||
ImageClassifierGraphOptionsProto *graphOptions =
|
ImageClassifierGraphOptionsProto *graphOptions =
|
||||||
optionsProto->MutableExtension(ImageClassifierGraphOptionsProto::ext);
|
optionsProto->MutableExtension(ImageClassifierGraphOptionsProto::ext);
|
||||||
[self.baseOptions copyToProto:graphOptions->mutable_base_options()];
|
|
||||||
|
[self.baseOptions copyToProto:graphOptions->mutable_base_options()
|
||||||
|
withUseStreamMode:self.runningMode != MPPRunningModeImage];
|
||||||
|
|
||||||
ClassifierOptionsProto *classifierOptionsProto = graphOptions->mutable_classifier_options();
|
ClassifierOptionsProto *classifierOptionsProto = graphOptions->mutable_classifier_options();
|
||||||
classifierOptionsProto->Clear();
|
classifierOptionsProto->Clear();
|
||||||
|
|
Loading…
Reference in New Issue
Block a user