Project import generated by Copybara.

PiperOrigin-RevId: 263889205
This commit is contained in:
MediaPipe Team
2019-08-16 18:56:48 -07:00
committed by jqtang
parent dc40414468
commit 294687295d
443 changed files with 33160 additions and 2011 deletions
+252
View File
@@ -0,0 +1,252 @@
package(default_visibility = ["//visibility:private"])
licenses(["notice"]) # Apache 2.0
cc_library(
name = "CFHolder",
# Header is excluded on non-ios so you can still build :all.
hdrs = select({
"//mediapipe:apple": ["CFHolder.h"],
"//conditions:default": [],
}),
visibility = ["//mediapipe/framework:mediapipe_internal"],
)
cc_library(
name = "util",
srcs = ["util.cc"],
hdrs = ["util.h"],
visibility = ["//mediapipe/framework:mediapipe_internal"],
deps = [
":CFHolder",
"//mediapipe/framework:calculator_framework",
"//mediapipe/framework/formats:image_frame",
"//mediapipe/framework/port:status",
],
)
objc_library(
name = "Weakify",
hdrs = ["Weakify.h"],
visibility = ["//mediapipe/framework:mediapipe_internal"],
)
MEDIAPIPE_IOS_SRCS = [
"MPPGraph.mm",
"MPPTimestampConverter.mm",
"NSError+util_status.mm",
]
MEDIAPIPE_IOS_HDRS = [
"MPPGraph.h",
"MPPTimestampConverter.h",
"NSError+util_status.h",
]
MEDIAPIPE_IOS_CC_DEPS = [
":CFHolder",
":util",
"//mediapipe/framework/port:map_util",
"//mediapipe/framework/port:ret_check",
"//mediapipe/framework/port:source_location",
"//mediapipe/framework/port:status",
"//mediapipe/framework/port:statusor",
"//mediapipe/framework/port:threadpool",
"//mediapipe/framework:calculator_framework",
"//mediapipe/framework:mediapipe_profiling",
"//mediapipe/gpu:MPPGraphGPUData",
"//mediapipe/gpu:pixel_buffer_pool_util",
"//mediapipe/gpu:gpu_buffer",
"//mediapipe/gpu:gl_base",
"//mediapipe/gpu:gpu_shared_data_internal",
"//mediapipe/gpu:graph_support",
# Other deps
"//mediapipe/util:cpu_util",
"@com_google_absl//absl/base:core_headers",
"@com_google_absl//absl/memory",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/synchronization",
]
objc_library(
name = "mediapipe_framework_ios",
srcs = MEDIAPIPE_IOS_SRCS,
hdrs = MEDIAPIPE_IOS_HDRS,
copts = [
"-Wno-shorten-64-to-32",
],
sdk_frameworks = [
# Needed for OpenCV.
"Accelerate",
],
visibility = ["//mediapipe/framework:mediapipe_internal"],
deps = MEDIAPIPE_IOS_CC_DEPS + [
# These are objc_library deps.
"@google_toolbox_for_mac//:GTM_Defines",
],
)
objc_library(
name = "mediapipe_input_sources_ios",
srcs = [
"MPPCameraInputSource.m",
"MPPDisplayLinkWeakTarget.m",
"MPPInputSource.m",
"MPPPlayerInputSource.m",
],
hdrs = [
"MPPCameraInputSource.h",
"MPPDisplayLinkWeakTarget.h",
"MPPInputSource.h",
"MPPPlayerInputSource.h",
],
visibility = ["//mediapipe/framework:mediapipe_internal"],
)
objc_library(
name = "mediapipe_gl_view_renderer",
srcs = [
"MPPGLViewRenderer.mm",
],
hdrs = [
"MPPGLViewRenderer.h",
],
copts = [
"-Wno-shorten-64-to-32",
],
visibility = ["//mediapipe/framework:mediapipe_internal"],
deps = [
":mediapipe_framework_ios",
"//mediapipe/gpu:gl_calculator_helper_ios",
"//mediapipe/gpu:gl_quad_renderer",
"//mediapipe/gpu:gl_simple_shaders",
],
)
objc_library(
name = "mediapipe_layer_renderer",
srcs = [
"MPPLayerRenderer.m",
],
hdrs = [
"MPPLayerRenderer.h",
],
copts = [
"-Wno-shorten-64-to-32",
],
visibility = ["//mediapipe/framework:mediapipe_internal"],
deps = [
":mediapipe_framework_ios",
":mediapipe_gl_view_renderer",
"//mediapipe/gpu:gl_calculator_helper_ios",
],
)
objc_library(
name = "CGImageRefUtils",
srcs = [
"CGImageRefUtils.mm",
],
hdrs = [
"CGImageRefUtils.h",
],
copts = [
"-Wno-shorten-64-to-32",
],
sdk_frameworks = [
"CoreVideo",
],
visibility = ["//mediapipe/framework:mediapipe_internal"],
deps = [
":mediapipe_framework_ios",
"@com_google_absl//absl/strings",
],
)
objc_library(
name = "MPPGraphTestBase",
testonly = 1,
srcs = [
"MPPGraphTestBase.mm",
],
hdrs = [
"MPPGraphTestBase.h",
],
copts = [
"-Wno-shorten-64-to-32",
],
sdk_frameworks = [
"Accelerate",
"AVFoundation",
"CoreVideo",
"CoreGraphics",
"CoreMedia",
"GLKit",
"OpenGLES",
"QuartzCore",
"UIKit",
],
visibility = ["//mediapipe/framework:mediapipe_internal"],
deps = [
":CGImageRefUtils",
":Weakify",
":mediapipe_framework_ios",
"//mediapipe/framework:calculator_framework",
],
)
objc_library(
name = "mediapipe_framework_ios_testLib",
testonly = 1,
srcs = [
"CFHolderTests.mm",
"MPPGraphTests.mm",
],
copts = [
"-Wno-shorten-64-to-32",
],
data = [
"testdata/googlelogo_color_272x92dp.png",
],
sdk_frameworks = [
"Accelerate",
"AVFoundation",
"CoreVideo",
"CoreGraphics",
"CoreMedia",
"GLKit",
"QuartzCore",
"UIKit",
],
visibility = ["//mediapipe/framework:mediapipe_internal"],
deps = [
":CGImageRefUtils",
":MPPGraphTestBase",
":Weakify",
":mediapipe_framework_ios",
"//mediapipe/calculators/core:pass_through_calculator",
],
)
load("//mediapipe/framework/tool:mediapipe_graph.bzl", "mediapipe_binary_graph")
[
mediapipe_binary_graph(
name = graph.split("/")[-1].rsplit(".", 1)[0] + "_graph",
graph = graph,
output_name = "%s.binarypb" % graph.split("/")[-1].rsplit(".", 1)[0],
visibility = ["//mediapipe/framework:mediapipe_internal"],
)
for graph in glob(["testdata/*.pbtxt"])
]
exports_files(
[
"testdata/googlelogo_color_272x92dp.png",
"testdata/googlelogo_color_272x92dp_luminance.png",
],
visibility = [
"//mediapipe/feature_extraction/video/video_effects:__pkg__",
"//mediapipe/gpu:__pkg__",
],
)
+109
View File
@@ -0,0 +1,109 @@
// Copyright 2019 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.
#ifndef MEDIAPIPE_OBJC_CFHOLDER_H_
#define MEDIAPIPE_OBJC_CFHOLDER_H_
#import <CoreFoundation/CoreFoundation.h>
/// Manages ownership of a CoreFoundation type (any type that can be passed
/// to CFRetain/CFRelease).
template <typename T>
class CFHolder {
public:
/// Default constructor gives a NULL ref.
CFHolder() : _object(NULL) {}
/// Constructor with the basic ref type. Retains it.
explicit CFHolder(T object) : _object(RetainIfNotNull(object)) {}
/// Copy constructor.
CFHolder(const CFHolder& other) : _object(RetainIfNotNull(*other)) {}
/// Move constructor.
CFHolder(CFHolder&& other) : _object(*other) { other._object = NULL; }
/// Destructor releases the held object.
~CFHolder() { ReleaseIfNotNull(_object); }
/// Dereference to access the held object.
T operator*() const { return _object; }
/// Assigning from another CFHolder adds a reference.
CFHolder& operator=(const CFHolder& other) { return reset(*other); }
/// Move assignment does not add a reference.
CFHolder& operator=(CFHolder&& other) {
// C++11 allows its library implementation to assume that rvalue reference
// arguments are not aliased. See 17.6.4.9 in the standard document.
ReleaseIfNotNull(_object);
_object = other._object;
other._object = NULL;
return *this;
}
/// Equality and inequality operators.
bool operator==(const CFHolder& other) const {
return _object == other._object;
}
bool operator!=(const CFHolder& other) const { return !operator==(other); }
bool operator==(T object) const { return _object == object; }
bool operator!=(T object) const { return !operator==(object); }
/// Sets the managed object.
CFHolder& reset(T object) {
T old = _object;
_object = RetainIfNotNull(object);
ReleaseIfNotNull(old);
return *this;
}
/// Takes ownership of the object. Does not retain.
CFHolder& adopt(T object) {
ReleaseIfNotNull(_object);
_object = object;
return *this;
}
private:
static inline T RetainIfNotNull(T object) {
if (object) CFRetain(object);
return object;
}
static inline void ReleaseIfNotNull(T object) {
if (object) CFRelease(object);
}
T _object;
};
/// Using these functions allows template argument deduction (i.e. you do not
/// need to specify the type of object the holder holds, it is inferred from
/// the argument.
template <typename T>
CFHolder<T>* NewCFHolder(T object) {
return new CFHolder<T>(object);
}
template <typename T>
CFHolder<T> MakeCFHolder(T object) {
return CFHolder<T>(object);
}
template <typename T>
CFHolder<T> MakeCFHolderAdopting(T object) {
return CFHolder<T>().adopt(object);
}
#endif // MEDIAPIPE_OBJC_CFHOLDER_H_
+175
View File
@@ -0,0 +1,175 @@
// Copyright 2019 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 <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#import "mediapipe/objc/CFHolder.h"
#include <utility>
@interface CFHolderTests : XCTestCase {
UInt8 _bytes[4];
CFDataRef _data;
}
@end
@implementation CFHolderTests
- (void)setUp {
_data = CFDataCreate(NULL, _bytes, sizeof(_bytes));
}
- (void)tearDown {
CFRelease(_data);
}
- (void)testCreateAndDestroy {
XCTAssertEqual(CFGetRetainCount(_data), 1);
{
CFHolder<CFDataRef> holder(_data);
XCTAssertEqual(CFGetRetainCount(_data), 2);
}
XCTAssertEqual(CFGetRetainCount(_data), 1);
}
- (void)testDereference {
CFHolder<CFDataRef> holder(_data);
XCTAssertEqual(*holder, _data);
}
- (void)testCopy {
XCTAssertEqual(CFGetRetainCount(_data), 1);
{
CFHolder<CFDataRef> holder(_data);
XCTAssertEqual(CFGetRetainCount(_data), 2);
{
CFHolder<CFDataRef> holder2(holder);
XCTAssertEqual(CFGetRetainCount(_data), 3);
{
CFHolder<CFDataRef> holder3 = holder;
XCTAssertEqual(CFGetRetainCount(_data), 4);
}
XCTAssertEqual(CFGetRetainCount(_data), 3);
}
XCTAssertEqual(CFGetRetainCount(_data), 2);
}
XCTAssertEqual(CFGetRetainCount(_data), 1);
}
- (void)testOverwriteWithNull {
XCTAssertEqual(CFGetRetainCount(_data), 1);
{
// Copy assignment.
CFHolder<CFDataRef> holder(_data);
XCTAssertEqual(CFGetRetainCount(_data), 2);
CFHolder<CFDataRef> holder2;
holder = holder2;
XCTAssertEqual(CFGetRetainCount(_data), 1);
}
{
// Move assignment.
CFHolder<CFDataRef> holder(_data);
XCTAssertEqual(CFGetRetainCount(_data), 2);
holder = CFHolder<CFDataRef>();
XCTAssertEqual(CFGetRetainCount(_data), 1);
}
}
- (void)testOverwriteWithOther {
CFDataRef data2 = CFDataCreate(NULL, _bytes, sizeof(_bytes));
XCTAssertEqual(CFGetRetainCount(_data), 1);
XCTAssertEqual(CFGetRetainCount(data2), 1);
{
// Copy assignment.
CFHolder<CFDataRef> holder(_data);
XCTAssertEqual(CFGetRetainCount(_data), 2);
CFHolder<CFDataRef> holder2(data2);
XCTAssertEqual(CFGetRetainCount(data2), 2);
holder = holder2;
XCTAssertEqual(CFGetRetainCount(_data), 1);
XCTAssertEqual(CFGetRetainCount(data2), 3);
}
{
// Move assignment.
CFHolder<CFDataRef> holder(_data);
XCTAssertEqual(CFGetRetainCount(_data), 2);
holder = CFHolder<CFDataRef>(data2);
XCTAssertEqual(CFGetRetainCount(_data), 1);
XCTAssertEqual(CFGetRetainCount(data2), 2);
}
CFRelease(data2);
}
- (void)testCompare {
CFDataRef data2 = CFDataCreate(NULL, _bytes, sizeof(_bytes));
CFHolder<CFDataRef> holder(_data);
CFHolder<CFDataRef> holdersame(_data);
CFHolder<CFDataRef> holderother(data2);
CFHolder<CFDataRef> empty;
// Compare with other holder.
XCTAssertEqual(holder, holder);
XCTAssertEqual(holder, holdersame);
XCTAssertNotEqual(holder, holderother);
XCTAssertNotEqual(holder, empty);
// Compare with held type.
XCTAssertEqual(holder, _data);
XCTAssertNotEqual(holder, data2);
XCTAssertNotEqual(holder, nil);
XCTAssertEqual(empty, nil);
XCTAssertNotEqual(empty, _data);
CFRelease(data2);
}
- (void)testReset {
XCTAssertEqual(CFGetRetainCount(_data), 1);
{
CFHolder<CFDataRef> holder;
holder.reset(_data);
XCTAssertEqual(CFGetRetainCount(_data), 2);
CFDataRef data2 = CFDataCreate(NULL, _bytes, sizeof(_bytes));
holder.reset(data2);
XCTAssertEqual(CFGetRetainCount(_data), 1);
XCTAssertEqual(CFGetRetainCount(data2), 2);
CFRelease(data2);
}
XCTAssertEqual(CFGetRetainCount(_data), 1);
}
- (void)testAdopt {
CFRetain(_data);
XCTAssertEqual(CFGetRetainCount(_data), 2);
{
CFHolder<CFDataRef> holder;
holder.adopt(_data);
XCTAssertEqual(CFGetRetainCount(_data), 2);
}
XCTAssertEqual(CFGetRetainCount(_data), 1);
}
- (void)testMove {
XCTAssertEqual(CFGetRetainCount(_data), 1);
{
CFHolder<CFDataRef> holder(_data);
XCTAssertEqual(CFGetRetainCount(_data), 2);
CFHolder<CFDataRef> holder2(std::move(holder));
XCTAssertEqual(CFGetRetainCount(_data), 2);
CFHolder<CFDataRef> holder3(std::move(holder2));
XCTAssertEqual(CFGetRetainCount(_data), 2);
}
XCTAssertEqual(CFGetRetainCount(_data), 1);
}
@end
+43
View File
@@ -0,0 +1,43 @@
// Copyright 2019 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.
#ifndef MEDIAPIPE_OBJC_CGIMAGEREFUTILS_H_
#define MEDIAPIPE_OBJC_CGIMAGEREFUTILS_H_
#import <CoreVideo/CoreVideo.h>
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
extern NSString *const kCGImageRefUtilsErrorDomain;
// TODO: Get rid of this library or make it a wrapper around util.h
// versions so that it can be used in pure Objective-C code.
/// Creates a CGImage with a copy of the contents of the CVPixelBuffer. Returns nil on error, if
/// the |error| argument is not nil, *error is set to an NSError describing the failure. Caller
/// is responsible for releasing the CGImage by calling CGImageRelease().
CGImageRef CreateCGImageFromCVPixelBuffer(CVPixelBufferRef imageBuffer, NSError **error);
/// Creates a CVPixelBuffer with a copy of the contents of the CGImage. Returns nil on error, if
/// the |error| argument is not nil, *error is set to an NSError describing the failure. Caller
/// is responsible for releasing the CVPixelBuffer by calling CVPixelBufferRelease.
CVPixelBufferRef CreateCVPixelBufferFromCGImage(CGImageRef image, NSError **error);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
#endif // MEDIAPIPE_OBJC_CGIMAGEREFUTILS_H_
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2019 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 "CGImageRefUtils.h"
#import <Foundation/Foundation.h>
#import "mediapipe/objc/CFHolder.h"
#import "mediapipe/objc/NSError+util_status.h"
#import "mediapipe/objc/util.h"
#include "mediapipe/framework/port/status.h"
CGImageRef CreateCGImageFromCVPixelBuffer(CVPixelBufferRef imageBuffer, NSError **error) {
CFHolder<CGImageRef> cg_image_holder;
::mediapipe::Status status = CreateCGImageFromCVPixelBuffer(imageBuffer, &cg_image_holder);
if (!status.ok()) {
*error = [NSError gus_errorWithStatus:status];
return nil;
}
CGImageRef cg_image = *cg_image_holder;
CGImageRetain(cg_image);
return cg_image;
}
CVPixelBufferRef CreateCVPixelBufferFromCGImage(CGImageRef image, NSError **error) {
CFHolder<CVPixelBufferRef> pixel_buffer_holder;
::mediapipe::Status status = CreateCVPixelBufferFromCGImage(image, &pixel_buffer_holder);
if (!status.ok()) {
*error = [NSError gus_errorWithStatus:status];
return nil;
}
CVPixelBufferRef pixel_buffer = *pixel_buffer_holder;
CVPixelBufferRetain(pixel_buffer);
return pixel_buffer;
}
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2019 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 <AVFoundation/AVFoundation.h>
#import "mediapipe/objc/MPPInputSource.h"
/// A source that obtains video frames from the camera.
@interface MPPCameraInputSource : MPPInputSource
/// Whether we are allowed to use the camera.
@property(nonatomic, getter=isAuthorized, readonly) BOOL authorized;
/// Session preset to use for capturing.
@property(nonatomic) NSString *sessionPreset;
/// Which camera on an iOS device to use, assuming iOS device with more than one camera.
@property(nonatomic) AVCaptureDevicePosition cameraPosition;
// Whether to use depth data or not
@property(nonatomic) BOOL useDepth;
/// Whether to rotate video buffers with device rotation.
@property(nonatomic) BOOL autoRotateBuffers;
/// The capture session.
@property(nonatomic, readonly) AVCaptureSession *session;
/// The capture video preview layer.
@property(nonatomic, readonly) AVCaptureVideoPreviewLayer *videoPreviewLayer;
/// The orientation of camera frame buffers.
@property(nonatomic) AVCaptureVideoOrientation orientation;
/// Prompts the user to grant camera access and provides the result as a BOOL to a completion
/// handler. Should be called after [MPPCameraInputSource init] and before
/// [MPPCameraInputSource start]. If the user has previously granted or denied permission, this
/// method simply returns the saved response to the permission request.
- (void)requestCameraAccessWithCompletionHandler:(void (^_Nullable)(BOOL granted))handler;
@end
+298
View File
@@ -0,0 +1,298 @@
// Copyright 2019 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 "MPPCameraInputSource.h"
#import <UIKit/UIKit.h>
@interface MPPCameraInputSource () <AVCaptureVideoDataOutputSampleBufferDelegate,
AVCaptureDepthDataOutputDelegate>
@end
@implementation MPPCameraInputSource {
AVCaptureSession* _session;
AVCaptureDeviceInput* _videoDeviceInput;
AVCaptureVideoDataOutput* _videoDataOutput;
AVCaptureDepthDataOutput* _depthDataOutput;
AVCaptureDevice *_currentDevice;
OSType _pixelFormatType;
BOOL _autoRotateBuffers;
BOOL _setupDone;
BOOL _useDepth;
BOOL _useCustomOrientation;
}
- (instancetype)init {
self = [super init];
if (self) {
_cameraPosition = AVCaptureDevicePositionBack;
_session = [[AVCaptureSession alloc] init];
_pixelFormatType = kCVPixelFormatType_32BGRA;
AVAuthorizationStatus status =
[AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
_authorized = status == AVAuthorizationStatusAuthorized;
}
return self;
}
- (void)setDelegate:(id<MPPInputSourceDelegate>)delegate
queue:(dispatch_queue_t)queue {
[super setDelegate:delegate queue:queue];
// Note that _depthDataOutput and _videoDataOutput may not have been created yet. In that case,
// this message to nil is ignored, and the delegate will be set later by setupCamera.
[_videoDataOutput setSampleBufferDelegate:self queue:queue];
[_depthDataOutput setDelegate:self callbackQueue:queue];
}
- (void)start {
if (!_setupDone) [self setupCamera];
if (_autoRotateBuffers) {
[self enableAutoRotateBufferObserver:YES];
}
[_session startRunning];
}
- (void)stop {
if (_autoRotateBuffers) {
[self enableAutoRotateBufferObserver:NO];
}
[_session stopRunning];
}
- (BOOL)isRunning {
return _session.isRunning;
}
- (void)setCameraPosition:(AVCaptureDevicePosition)cameraPosition {
BOOL wasRunning = [self isRunning];
if (wasRunning) {
[self stop];
}
_cameraPosition = cameraPosition;
_setupDone = NO;
if (wasRunning) {
[self start];
}
}
- (void)setUseDepth:(BOOL)useDepth {
if (useDepth == _useDepth) {
return;
}
BOOL wasRunning = [self isRunning];
if (wasRunning) {
[self stop];
}
_useDepth = useDepth;
_setupDone = NO;
if (wasRunning) {
[self start];
}
}
- (void)setOrientation:(AVCaptureVideoOrientation)orientation {
if (orientation == _orientation) {
return;
}
BOOL wasRunning = [self isRunning];
if (wasRunning) {
[self stop];
}
_orientation = orientation;
_useCustomOrientation = YES;
_setupDone = NO;
if (wasRunning) {
[self start];
}
}
- (void)setAutoRotateBuffers:(BOOL)autoRotateBuffers {
if (autoRotateBuffers == _autoRotateBuffers) {
return; // State has not changed.
}
_autoRotateBuffers = autoRotateBuffers;
if ([self isRunning]) {
// Enable or disable observer this settings changes while this input source is running.
[self enableAutoRotateBufferObserver:_autoRotateBuffers];
}
}
- (void)enableAutoRotateBufferObserver:(BOOL)enable {
if (enable) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deviceOrientationChanged)
name:UIDeviceOrientationDidChangeNotification
object:nil];
// Trigger a device orientation change instead of waiting for the first change.
[self deviceOrientationChanged];
} else {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIDeviceOrientationDidChangeNotification
object:nil];
}
}
- (OSType)pixelFormatType {
return _pixelFormatType;
}
- (void)setPixelFormatType:(OSType)pixelFormatType {
_pixelFormatType = pixelFormatType;
if ([self isRunning]) {
_videoDataOutput.videoSettings = @{
(id)kCVPixelBufferPixelFormatTypeKey : @(_pixelFormatType)
};
}
}
#pragma mark - Camera-specific methods
- (NSString*)sessionPreset {
return _session.sessionPreset;
}
- (void)setSessionPreset:(NSString*)sessionPreset {
_session.sessionPreset = sessionPreset;
}
- (void)setupCamera {
NSError* error = nil;
if (_videoDeviceInput) {
[_session removeInput:_videoDeviceInput];
}
AVCaptureDeviceDiscoverySession* deviceDiscoverySession = [AVCaptureDeviceDiscoverySession
discoverySessionWithDeviceTypes:@[
_cameraPosition == AVCaptureDevicePositionFront && _useDepth ?
AVCaptureDeviceTypeBuiltInTrueDepthCamera :
AVCaptureDeviceTypeBuiltInWideAngleCamera]
mediaType:AVMediaTypeVideo
position:_cameraPosition];
AVCaptureDevice* videoDevice =
[deviceDiscoverySession devices]
? [deviceDiscoverySession devices].firstObject
: [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
_videoDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
if (error) {
NSLog(@"%@", error);
return;
}
[_session addInput:_videoDeviceInput];
if (!_videoDataOutput) {
_videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
[_session addOutput:_videoDataOutput];
// Set this when we have a handler.
if (self.delegateQueue)
[_videoDataOutput setSampleBufferDelegate:self queue:self.delegateQueue];
_videoDataOutput.alwaysDiscardsLateVideoFrames = YES;
// Only a few pixel formats are available for capture output:
// kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
// kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
// kCVPixelFormatType_32BGRA.
_videoDataOutput.videoSettings = @{
(id)kCVPixelBufferPixelFormatTypeKey : @(_pixelFormatType)
};
}
// Remove Old Depth Depth
if (_depthDataOutput) {
[_session removeOutput:_depthDataOutput];
}
if (_useDepth) {
// Add Depth Output
_depthDataOutput = [[AVCaptureDepthDataOutput alloc] init];
_depthDataOutput.alwaysDiscardsLateDepthData = YES;
if ([_session canAddOutput:_depthDataOutput]) {
[_session addOutput:_depthDataOutput];
AVCaptureConnection* connection =
[_depthDataOutput connectionWithMediaType:AVMediaTypeDepthData];
if (connection != nil)
connection.enabled = true;
// Set this when we have a handler.
if (self.delegateQueue) {
[_depthDataOutput setDelegate:self callbackQueue:self.delegateQueue];
}
}
else
_depthDataOutput = nil;
}
if (_useCustomOrientation) {
AVCaptureConnection* connection = [_videoDataOutput connectionWithMediaType:AVMediaTypeVideo];
connection.videoOrientation = _orientation;
}
_setupDone = YES;
}
- (void)requestCameraAccessWithCompletionHandler:(void (^)(BOOL))handler {
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo
completionHandler:^(BOOL granted) {
_authorized = granted;
if (handler) {
handler(granted);
}
}];
}
#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate methods
// Receives frames from the camera. Invoked on self.frameHandlerQueue.
- (void)captureOutput:(AVCaptureOutput*)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection*)connection {
CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
if ([self.delegate respondsToSelector:@selector(processVideoFrame:timestamp:fromSource:)]) {
[self.delegate processVideoFrame:imageBuffer timestamp:timestamp fromSource:self];
} else if ([self.delegate respondsToSelector:@selector(processVideoFrame:fromSource:)]) {
[self.delegate processVideoFrame:imageBuffer fromSource:self];
}
}
#pragma mark - AVCaptureDepthDataOutputDelegate methods
// Receives depth frames from the camera. Invoked on self.frameHandlerQueue.
- (void)depthDataOutput:(AVCaptureDepthDataOutput *)output
didOutputDepthData:(AVDepthData *)depthData
timestamp:(CMTime)timestamp
connection:(AVCaptureConnection *)connection {
if (depthData.depthDataType != kCVPixelFormatType_DepthFloat32) {
depthData = [depthData depthDataByConvertingToDepthDataType:kCVPixelFormatType_DepthFloat32];
}
CVPixelBufferRef depthBuffer = depthData.depthDataMap;
[self.delegate processDepthData:depthData timestamp:timestamp fromSource:self];
}
#pragma mark - NSNotificationCenter event handlers
- (void)deviceOrientationChanged {
AVCaptureConnection* connection = [_videoDataOutput connectionWithMediaType:AVMediaTypeVideo];
connection.videoOrientation = (AVCaptureVideoOrientation)[UIDevice currentDevice].orientation;
}
@end
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2019 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 <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>
/// A generic target/callback holder. Useful for indirectly using DisplayLink and allowing the
/// complete deletion of displaylink reference holders.
@interface MPPDisplayLinkWeakTarget : NSObject
- (instancetype)initWithTarget:(id)target selector:(SEL)sel;
- (void)displayLinkCallback:(CADisplayLink *)sender;
@end
+40
View File
@@ -0,0 +1,40 @@
// Copyright 2019 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/objc/MPPDisplayLinkWeakTarget.h"
@implementation MPPDisplayLinkWeakTarget {
__weak id _target;
SEL _selector;
}
#pragma mark - Init
- (instancetype)initWithTarget:(id)target selector:(SEL)sel {
self = [super init];
if (self) {
_target = target;
_selector = sel;
}
return self;
}
#pragma mark - Public
- (void)displayLinkCallback:(CADisplayLink *)sender {
void (*display)(id, SEL, CADisplayLink *) = (void *)[_target methodForSelector:_selector];
display(_target, _selector, sender);
}
@end
+68
View File
@@ -0,0 +1,68 @@
// Copyright 2019 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 <GLKit/GLKit.h>
/// Modes of clockwise rotation for input frames.
typedef enum {
MediaPipeFrameRotationNone,
MediaPipeFrameRotation90,
MediaPipeFrameRotation180,
MediaPipeFrameRotation270
} MediaPipeFrameRotationMode;
typedef enum {
// Scale the frame up to fit the drawing area, preserving aspect ratio; may letterbox.
MediaPipeFrameScaleFit,
// Scale the frame up to fill the drawing area, preserving aspect ratio; may crop.
MediaPipeFrameScaleFillAndCrop,
} MediaPipeFrameScaleMode;
/// Renders frames in a GLKView.
@interface MPPGLViewRenderer : NSObject <GLKViewDelegate>
/// Rendering context for display.
@property(nonatomic) EAGLContext *glContext;
/// The frame to be rendered next. This pixel buffer must be unlocked, and
/// should not be modified after handing it to the renderer.
@property(atomic, retain) __attribute__((NSObject)) CVPixelBufferRef nextPixelBufferToRender;
/// When YES, the last drawn pixel buffer is retained by this object after it is drawn in the GLView
/// for which it is a delegate. Otherwise it is released after it has been rendered.
/// Set this property to YES when your GLView can be redrawn with the same pixel buffer, such as
/// during an animation.
@property(nonatomic, assign) BOOL retainsLastPixelBuffer;
/// Sets which way to rotate input frames before rendering them.
/// Default value is MediaPipeFrameRotationNone.
/// Note that changing the transform property of a GLKView once rendering has
/// started causes problems inside GLKView. Instead, we perform the rotation
/// in our rendering code.
@property(nonatomic) MediaPipeFrameRotationMode frameRotationMode;
/// Sets how to scale the frame within the view.
/// Default value is MediaPipeFrameScaleScaleToFit.
@property(nonatomic) MediaPipeFrameScaleMode frameScaleMode;
/// If YES, swap left and right. Useful for the front camera.
@property(nonatomic) BOOL mirrored;
/// Draws a pixel buffer to its context with the specified view size.
- (void)drawPixelBuffer:(CVPixelBufferRef)pixelBuffer
width:(GLfloat)viewWidth
height:(GLfloat)viewHeight;
@end
+189
View File
@@ -0,0 +1,189 @@
// Copyright 2019 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 "MPPGLViewRenderer.h"
#include <OpenGLES/ES2/gl.h>
#include <OpenGLES/ES2/glext.h>
#include <algorithm>
#include <libkern/OSAtomic.h>
#include "absl/memory/memory.h"
#include "mediapipe/gpu/gl_quad_renderer.h"
#include "mediapipe/gpu/gl_simple_shaders.h"
#include "mediapipe/gpu/shader_util.h"
#import "mediapipe/objc/NSError+util_status.h"
#import "GTMDefines.h"
@implementation MPPGLViewRenderer {
/// Used to create textures for the pixel buffers to be rendered.
/// The use of this class allows the GPU to access the buffer's memory directly,
/// without a memory copy.
CVOpenGLESTextureCacheRef _textureCache;
/// Internal renderer.
std::unique_ptr<mediapipe::QuadRenderer> renderer_;
/// Used to synchronize access to _nextPixelBufferToRender.
OSSpinLock _bufferLock;
volatile CVPixelBufferRef _nextPixelBufferToRender;
}
- (instancetype)init {
self = [super init];
if (self) {
_glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
_bufferLock = OS_SPINLOCK_INIT;
_frameRotationMode = MediaPipeFrameRotationNone;
_frameScaleMode = MediaPipeFrameScaleFit;
}
return self;
}
- (void)dealloc {
if (_textureCache) CFRelease(_textureCache);
CVPixelBufferRelease(_nextPixelBufferToRender);
// Fixes crash during dealloc that only happens in iOS 9. More info at b/67095363.
if ([EAGLContext currentContext] == _glContext) {
[EAGLContext setCurrentContext:nil];
}
}
- (CVPixelBufferRef)nextPixelBufferToRender {
OSSpinLockLock(&_bufferLock);
CVPixelBufferRef buffer = _nextPixelBufferToRender;
OSSpinLockUnlock(&_bufferLock);
return buffer;
}
- (void)setNextPixelBufferToRender:(CVPixelBufferRef)buffer {
OSSpinLockLock(&_bufferLock);
if (_nextPixelBufferToRender != buffer) {
CVPixelBufferRelease(_nextPixelBufferToRender);
_nextPixelBufferToRender = buffer;
CVPixelBufferRetain(_nextPixelBufferToRender);
}
OSSpinLockUnlock(&_bufferLock);
}
- (void)setupGL {
CVReturn err;
err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, _glContext, NULL, &_textureCache);
_GTMDevAssert(err == kCVReturnSuccess,
@"CVOpenGLESTextureCacheCreate failed: %d", err);
renderer_ = absl::make_unique<mediapipe::QuadRenderer>();
auto status = renderer_->GlSetup();
_GTMDevAssert(status.ok(),
@"renderer setup failed: %@", [NSError gus_errorWithStatus:status]);
}
mediapipe::FrameScaleMode InternalScaleMode(MediaPipeFrameScaleMode mode) {
switch (mode) {
case MediaPipeFrameScaleFit:
return mediapipe::FrameScaleMode::kFit;
case MediaPipeFrameScaleFillAndCrop:
return mediapipe::FrameScaleMode::kFillAndCrop;
}
}
mediapipe::FrameRotation InternalRotationMode(MediaPipeFrameRotationMode rot) {
switch (rot) {
case MediaPipeFrameRotationNone:
return mediapipe::FrameRotation::kNone;
case MediaPipeFrameRotation90:
return mediapipe::FrameRotation::k90;
case MediaPipeFrameRotation180:
return mediapipe::FrameRotation::k180;
case MediaPipeFrameRotation270:
return mediapipe::FrameRotation::k270;
}
}
- (void)drawPixelBuffer:(CVPixelBufferRef)pixelBuffer
width:(GLfloat)viewWidth
height:(GLfloat)viewHeight {
if (!_textureCache) [self setupGL];
size_t frameWidth = CVPixelBufferGetWidth(pixelBuffer);
size_t frameHeight = CVPixelBufferGetHeight(pixelBuffer);
CVOpenGLESTextureRef texture = NULL;
OSType pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);
CVReturn error;
if (pixelFormat == kCVPixelFormatType_OneComponent8) {
error = CVOpenGLESTextureCacheCreateTextureFromImage(
kCFAllocatorDefault, _textureCache, pixelBuffer, NULL,
GL_TEXTURE_2D, GL_LUMINANCE, (GLsizei)frameWidth, (GLsizei)frameHeight,
GL_LUMINANCE, GL_UNSIGNED_BYTE, 0, &texture);
} else if (pixelFormat == kCVPixelFormatType_OneComponent32Float) {
error = CVOpenGLESTextureCacheCreateTextureFromImage(
kCFAllocatorDefault, _textureCache, pixelBuffer, NULL,
GL_TEXTURE_2D, GL_LUMINANCE, (GLsizei)frameWidth, (GLsizei)frameHeight,
GL_LUMINANCE, GL_FLOAT, 0, &texture);
} else {
error = CVOpenGLESTextureCacheCreateTextureFromImage(
kCFAllocatorDefault, _textureCache, pixelBuffer, NULL,
GL_TEXTURE_2D, GL_RGBA, (GLsizei)frameWidth, (GLsizei)frameHeight,
GL_BGRA, GL_UNSIGNED_BYTE, 0, &texture);
}
_GTMDevAssert(error == kCVReturnSuccess,
@"CVOpenGLESTextureCacheCreateTextureFromImage failed: %d", error);
glClear(GL_COLOR_BUFFER_BIT);
glActiveTexture(GL_TEXTURE1);
glBindTexture(CVOpenGLESTextureGetTarget(texture), CVOpenGLESTextureGetName(texture));
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// Note: we perform a vertical flip by swapping the top and bottom coordinates.
// CVPixelBuffers have a top left origin and OpenGL has a bottom left origin.
auto status = renderer_->GlRender(
frameWidth, frameHeight, viewWidth, viewHeight,
InternalScaleMode(_frameScaleMode),
InternalRotationMode(_frameRotationMode),
_mirrored, /*flip_vertical=*/false, /*flip_texture=*/true);
_GTMDevAssert(status.ok(),
@"render failed: %@", [NSError gus_errorWithStatus:status]);
glBindTexture(CVOpenGLESTextureGetTarget(texture), 0);
glBindTexture(GL_TEXTURE_2D, 0 );
CFRelease(texture);
CVOpenGLESTextureCacheFlush(_textureCache, 0);
}
#pragma mark - GLKViewDelegate
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
CVPixelBufferRef pixelBuffer = NULL;
OSSpinLockLock(&_bufferLock);
pixelBuffer = _nextPixelBufferToRender;
if (_retainsLastPixelBuffer) {
CVPixelBufferRetain(pixelBuffer);
} else {
_nextPixelBufferToRender = NULL;
}
OSSpinLockUnlock(&_bufferLock);
if (!pixelBuffer) return;
[self drawPixelBuffer:pixelBuffer width:view.drawableWidth height:view.drawableHeight];
CVPixelBufferRelease(pixelBuffer);
}
@end
+81
View File
@@ -0,0 +1,81 @@
// Copyright 2019 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/objc/MPPGraph.h"
#import "mediapipe/objc/MPPGraphTestBase.h"
@interface MPPGpuSimpleTest : MPPGraphTestBase
@end
@implementation MPPGpuSimpleTest{
CFHolder<CVPixelBufferRef> _inputPixelBuffer;
CFHolder<CVPixelBufferRef> _referencePixelBuffer;
CFHolder<CVPixelBufferRef> _outputPixelBuffer;
}
- (void)setUp {
[super setUp];
UIImage* image = [self testImageNamed:@"sergey" extension:@"png"];
XCTAssertTrue(CreateCVPixelBufferFromCGImage(image.CGImage, &_inputPixelBuffer).ok());
image = [self testImageNamed:@"sobel_reference" extension:@"png"];
XCTAssertTrue(CreateCVPixelBufferFromCGImage(image.CGImage, &_referencePixelBuffer).ok());
}
// This delegate method receives output.
- (void)mediapipeGraph:(MPPGraph*)graph
didOutputPixelBuffer:(CVPixelBufferRef)pixelBuffer
fromStream:(const std::string&)streamName
timestamp:(const mediapipe::Timestamp&)timestamp {
NSLog(@"CALLBACK INVOKED");
_outputPixelBuffer.reset(pixelBuffer);
}
- (void)testSimpleGpuGraph {
// Graph setup.
NSData* configData = [self testDataNamed:@"test_sobel.binarypb" extension:nil];
mediapipe::CalculatorGraphConfig config;
XCTAssertTrue(config.ParseFromArray([configData bytes], [configData length]));
MPPGraph* mediapipeGraph = [[MPPGraph alloc] initWithGraphConfig:config];
// We receive output by setting ourselves as the delegate.
mediapipeGraph.delegate = self;
[mediapipeGraph addFrameOutputStream:"output_video" outputPacketType:MediaPipePacketPixelBuffer];
// Start running the graph.
NSError *error;
BOOL success = [mediapipeGraph startWithError:&error];
XCTAssertTrue(success, @"%@", error.localizedDescription);
// Send a frame.
XCTAssertTrue([mediapipeGraph sendPixelBuffer:*_inputPixelBuffer
intoStream:"input_video"
packetType:MediaPipePacketPixelBuffer
timestamp:mediapipe::Timestamp(0)]);
// Shut down the graph.
success = [mediapipeGraph closeAllInputStreamsWithError:&error];
XCTAssertTrue(success, @"%@", error.localizedDescription);
success = [mediapipeGraph waitUntilDoneWithError:&error];
XCTAssertTrue(success, @"%@", error.localizedDescription);
// Check output.
XCTAssertTrue(_outputPixelBuffer != nullptr);
[self savePixelBufferToSponge:*_outputPixelBuffer
withSubpath:@"sobel.png"];
XCTAssertTrue([self pixelBuffer:*_outputPixelBuffer
isCloseTo:*_referencePixelBuffer
maxLocalDifference:5
maxAverageDifference:FLT_MAX]);
}
@end
+214
View File
@@ -0,0 +1,214 @@
// Copyright 2019 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 <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>
#ifndef __cplusplus
#error This header can only be included by an Objective-C++ file.
#endif
#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/objc/util.h"
@class MPPGraph;
namespace mediapipe {
struct GpuSharedData;
} // namespace mediapipe
/// A delegate that can receive frames from a MediaPipe graph.
@protocol MPPGraphDelegate <NSObject>
/// Provides the delegate with a new video frame.
@optional
- (void)mediapipeGraph:(MPPGraph*)graph
didOutputPixelBuffer:(CVPixelBufferRef)pixelBuffer
fromStream:(const std::string&)streamName;
/// Provides the delegate with a new video frame and time stamp.
@optional
- (void)mediapipeGraph:(MPPGraph*)graph
didOutputPixelBuffer:(CVPixelBufferRef)pixelBuffer
fromStream:(const std::string&)streamName
timestamp:(const mediapipe::Timestamp&)timestamp;
/// Provides the delegate with a raw packet.
@optional
- (void)mediapipeGraph:(MPPGraph*)graph
didOutputPacket:(const mediapipe::Packet&)packet
fromStream:(const std::string&)streamName;
@end
/// Chooses the packet type used by MPPGraph to send and receive packets
/// from the graph.
typedef NS_ENUM(int, MediaPipePacketType) {
/// Any packet type.
/// Calls mediapipeGraph:didOutputPacket:fromStream:
MediaPipePacketRaw,
/// CFHolder<CVPixelBufferRef>.
/// Calls mediapipeGraph:didOutputPixelBuffer:fromStream:
/// Use this packet type to pass GPU frames to calculators.
MediaPipePacketPixelBuffer,
/// ImageFrame.
/// Calls mediapipeGraph:didOutputPixelBuffer:fromStream:
MediaPipePacketImageFrame,
/// RGBA ImageFrame, but do not swap the channels if the input pixel buffer
/// is BGRA. This is useful when the graph needs RGBA ImageFrames, but the
/// calculators do not care about the order of the channels, so BGRA data can
/// be used as-is.
/// Calls mediapipeGraph:didOutputPixelBuffer:fromStream:
MediaPipePacketImageFrameBGRANoSwap,
};
/// This class is an Objective-C wrapper around a MediaPipe graph object, and
/// helps interface it with iOS technologies such as AVFoundation.
@interface MPPGraph : NSObject
/// The delegate, which receives output frames.
@property(weak) id<MPPGraphDelegate> delegate;
/// If the graph is already processing more than this number of frames, drop any
/// new incoming frames. Used to avoid swamping slower devices when processing
/// cannot keep up with the speed of video input.
/// This works as long as frames are sent or received using these methods:
/// - sendPixelBuffer:intoStream:packetType:[timestamp:]
/// - addFrameOutputStream:outputPacketType:
/// Set to 0 (the default) for no limit.
@property(nonatomic) int maxFramesInFlight;
/// Determines whether adding a packet to an input stream whose queue is full
/// should fail or wait.
@property mediapipe::CalculatorGraph::GraphInputStreamAddMode packetAddMode;
- (instancetype)init NS_UNAVAILABLE;
/// Copies the config and initializes the graph.
/// @param config The configuration describing the graph.
- (instancetype)initWithGraphConfig:(const mediapipe::CalculatorGraphConfig&)config
NS_DESIGNATED_INITIALIZER;
- (mediapipe::ProfilingContext*)getProfiler;
/// Sets a stream header. If the header was already set, it is overwritten.
/// @param packet The header.
/// @param streamName The name of the stream.
- (void)setHeaderPacket:(const mediapipe::Packet&)packet forStream:(const std::string&)streamName;
/// Sets a side packet. If it was already set, it is overwritten.
/// Must be called before the graph is started.
/// @param packet The packet to be associated with the input side packet.
/// @param name The name of the input side packet.
- (void)setSidePacket:(const mediapipe::Packet&)packet named:(const std::string&)name;
/// Adds input side packets from a map. Any inputs that were already set are
/// left unchanged.
/// Must be called before the graph is started.
/// @param extraInputSidePackets The input side packets to be added.
- (void)addSidePackets:(const std::map<std::string, mediapipe::Packet>&)extraSidePackets;
// TODO: rename to addDelegateOutputStream:packetType:
/// Add an output stream in the graph from which the delegate wants to receive
/// output. The delegate method called depends on the provided packetType.
/// @param outputStreamName The name of the output stream from which
/// the delegate will receive frames.
/// @param packetType The type of packet provided by the output streams.
- (void)addFrameOutputStream:(const std::string&)outputStreamName
outputPacketType:(MediaPipePacketType)packetType;
/// Starts running the graph.
/// @return YES if successful.
- (BOOL)startWithError:(NSError**)error;
/// Sends a generic packet into a graph input stream.
/// The graph must have been started before calling this.
/// Returns YES if the packet was successfully sent.
- (BOOL)sendPacket:(const mediapipe::Packet&)packet
intoStream:(const std::string&)streamName
error:(NSError**)error;
- (BOOL)movePacket:(mediapipe::Packet&&)packet
intoStream:(const std::string&)streamName
error:(NSError**)error;
/// Sets the maximum queue size for a stream. Experimental feature, currently
/// only supported for graph input streams. Should be called before starting the
/// graph.
- (BOOL)setMaxQueueSize:(int)maxQueueSize
forStream:(const std::string&)streamName
error:(NSError**)error;
/// Creates a MediaPipe packet wrapping the given pixelBuffer;
- (mediapipe::Packet)packetWithPixelBuffer:(CVPixelBufferRef)pixelBuffer
packetType:(MediaPipePacketType)packetType;
/// Sends a pixel buffer into a graph input stream, using the specified packet
/// type. The graph must have been started before calling this. Drops frames and
/// returns NO if maxFramesInFlight is exceeded. If allowOverwrite is set to YES,
/// allows MediaPipe to overwrite the packet contents on successful sending for
/// possibly increased efficiency. Returns YES if the packet was successfully sent.
- (BOOL)sendPixelBuffer:(CVPixelBufferRef)imageBuffer
intoStream:(const std::string&)inputName
packetType:(MediaPipePacketType)packetType
timestamp:(const mediapipe::Timestamp&)timestamp
allowOverwrite:(BOOL)allowOverwrite;
/// Sends a pixel buffer into a graph input stream, using the specified packet
/// type. The graph must have been started before calling this. Drops frames and
/// returns NO if maxFramesInFlight is exceeded. Returns YES if the packet was
/// successfully sent.
- (BOOL)sendPixelBuffer:(CVPixelBufferRef)pixelBuffer
intoStream:(const std::string&)inputName
packetType:(MediaPipePacketType)packetType
timestamp:(const mediapipe::Timestamp&)timestamp;
/// Sends a pixel buffer into a graph input stream, using the specified packet
/// type. The graph must have been started before calling this. The timestamp is
/// automatically incremented from the last timestamp used by this method. Drops
/// frames and returns NO if maxFramesInFlight is exceeded. Returns YES if the
/// packet was successfully sent.
- (BOOL)sendPixelBuffer:(CVPixelBufferRef)pixelBuffer
intoStream:(const std::string&)inputName
packetType:(MediaPipePacketType)packetType;
/// Cancels a graph run. You must still call waitUntilDoneWithError: after this.
- (void)cancel;
/// Check if the graph contains this input stream
- (BOOL)hasInputStream:(const std::string&)inputName;
/// Closes an input stream.
/// You must close all graph input streams before stopping the graph.
/// @return YES if successful.
- (BOOL)closeInputStream:(const std::string&)inputName error:(NSError**)error;
/// Closes all graph input streams.
/// @return YES if successful.
- (BOOL)closeAllInputStreamsWithError:(NSError**)error;
/// Stops running the graph.
/// Call this before releasing this object. All input streams must have been
/// closed. This call does not time out, so you should not call it from the main
/// thread.
/// @return YES if successful.
- (BOOL)waitUntilDoneWithError:(NSError**)error;
/// Waits for the graph to become idle.
- (BOOL)waitUntilIdleWithError:(NSError**)error;
@end
+373
View File
@@ -0,0 +1,373 @@
// Copyright 2019 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/objc/MPPGraph.h"
#import <AVFoundation/AVFoundation.h>
#import <Accelerate/Accelerate.h>
#include <atomic>
#include "absl/memory/memory.h"
#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/framework/formats/image_frame.h"
#include "mediapipe/gpu/MPPGraphGPUData.h"
#include "mediapipe/gpu/gl_base.h"
#include "mediapipe/gpu/gpu_shared_data_internal.h"
#import "mediapipe/objc/NSError+util_status.h"
#import "GTMDefines.h"
@implementation MPPGraph {
// Graph is wrapped in a unique_ptr because it was generating 39+KB of unnecessary ObjC runtime
// information. See https://medium.com/@dmaclach/objective-c-encoding-and-you-866624cc02de
// for details.
std::unique_ptr<mediapipe::CalculatorGraph> _graph;
/// Input side packets that will be added to the graph when it is started.
std::map<std::string, mediapipe::Packet> _inputSidePackets;
/// Packet headers that will be added to the graph when it is started.
std::map<std::string, mediapipe::Packet> _streamHeaders;
/// Number of frames currently being processed by the graph.
std::atomic<int32_t> _framesInFlight;
/// Used as a sequential timestamp for MediaPipe.
mediapipe::Timestamp _frameTimestamp;
int64 _frameNumber;
// Graph config modified to expose requested output streams.
mediapipe::CalculatorGraphConfig _config;
// Tracks whether the graph has been started and is currently running.
BOOL _started;
}
- (instancetype)initWithGraphConfig:(const mediapipe::CalculatorGraphConfig&)config {
self = [super init];
if (self) {
// Turn on Cocoa multithreading, since MediaPipe uses threads.
// Not needed on iOS, but we may want to have OS X clients in the future.
[[[NSThread alloc] init] start];
_graph = absl::make_unique<mediapipe::CalculatorGraph>();
_config = config;
}
return self;
}
- (mediapipe::ProfilingContext*)getProfiler {
return _graph->profiler();
}
- (mediapipe::CalculatorGraph::GraphInputStreamAddMode)packetAddMode {
return _graph->GetGraphInputStreamAddMode();
}
- (void)setPacketAddMode:(mediapipe::CalculatorGraph::GraphInputStreamAddMode)mode {
_graph->SetGraphInputStreamAddMode(mode);
}
- (void)addFrameOutputStream:(const std::string&)outputStreamName
outputPacketType:(MediaPipePacketType)packetType {
std::string callbackInputName;
mediapipe::tool::AddCallbackCalculator(outputStreamName, &_config, &callbackInputName,
/*use_std_function=*/true);
// No matter what ownership qualifiers are put on the pointer, NewPermanentCallback will
// still end up with a strong pointer to MPPGraph*. That is why we use void* instead.
void* wrapperVoid = (__bridge void*)self;
_inputSidePackets[callbackInputName] =
mediapipe::MakePacket<std::function<void(const mediapipe::Packet&)>>(
[wrapperVoid, outputStreamName, packetType](const mediapipe::Packet& packet) {
CallFrameDelegate(wrapperVoid, outputStreamName, packetType, packet);
});
}
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p; framesInFlight = %d>", [self class], self,
_framesInFlight.load(std::memory_order_relaxed)];
}
/// This is the function that gets called by the CallbackCalculator that
/// receives the graph's output.
void CallFrameDelegate(void* wrapperVoid, const std::string& streamName,
MediaPipePacketType packetType, const mediapipe::Packet& packet) {
MPPGraph* wrapper = (__bridge MPPGraph*)wrapperVoid;
@autoreleasepool {
if (packetType == MediaPipePacketRaw) {
[wrapper.delegate mediapipeGraph:wrapper
didOutputPacket:packet
fromStream:streamName];
} else if (packetType == MediaPipePacketImageFrame) {
const auto& frame = packet.Get<mediapipe::ImageFrame>();
mediapipe::ImageFormat::Format format = frame.Format();
if (format == mediapipe::ImageFormat::SRGBA ||
format == mediapipe::ImageFormat::GRAY8) {
CVPixelBufferRef pixelBuffer;
// To ensure compatibility with CVOpenGLESTextureCache, this attribute should be present.
NSDictionary* attributes = @{
(id)kCVPixelBufferIOSurfacePropertiesKey : @{},
};
// If kCVPixelFormatType_32RGBA does not work, it returns kCVReturnInvalidPixelFormat.
CVReturn error = CVPixelBufferCreate(
NULL, frame.Width(), frame.Height(), kCVPixelFormatType_32BGRA,
(__bridge CFDictionaryRef)attributes, &pixelBuffer);
_GTMDevAssert(error == kCVReturnSuccess, @"CVPixelBufferCreate failed: %d", error);
error = CVPixelBufferLockBaseAddress(pixelBuffer, 0);
_GTMDevAssert(error == kCVReturnSuccess, @"CVPixelBufferLockBaseAddress failed: %d", error);
vImage_Buffer vDestination = vImageForCVPixelBuffer(pixelBuffer);
// Note: we have to throw away const here, but we should not overwrite
// the packet data.
vImage_Buffer vSource = vImageForImageFrame(frame);
if (format == mediapipe::ImageFormat::SRGBA) {
// Swap R and B channels.
const uint8_t permuteMap[4] = {2, 1, 0, 3};
vImage_Error vError = vImagePermuteChannels_ARGB8888(
&vSource, &vDestination, permuteMap, kvImageNoFlags);
_GTMDevAssert(vError == kvImageNoError, @"vImagePermuteChannels failed: %zd", vError);
} else {
// Convert grayscale back to BGRA
vImage_Error vError = vImageGrayToBGRA(&vSource, &vDestination);
_GTMDevAssert(vError == kvImageNoError, @"vImageGrayToBGRA failed: %zd", vError);
}
error = CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
_GTMDevAssert(error == kCVReturnSuccess,
@"CVPixelBufferUnlockBaseAddress failed: %d", error);
if ([wrapper.delegate respondsToSelector:@selector
(mediapipeGraph:didOutputPixelBuffer:fromStream:timestamp:)]) {
[wrapper.delegate mediapipeGraph:wrapper
didOutputPixelBuffer:pixelBuffer
fromStream:streamName
timestamp:packet.Timestamp()];
} else if ([wrapper.delegate respondsToSelector:@selector
(mediapipeGraph:didOutputPixelBuffer:fromStream:)]) {
[wrapper.delegate mediapipeGraph:wrapper
didOutputPixelBuffer:pixelBuffer
fromStream:streamName];
}
CVPixelBufferRelease(pixelBuffer);
} else {
_GTMDevLog(@"unsupported ImageFormat: %d", format);
}
#if MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
} else if (packetType == MediaPipePacketPixelBuffer) {
CVPixelBufferRef pixelBuffer = packet.Get<mediapipe::GpuBuffer>().GetCVPixelBufferRef();
if ([wrapper.delegate
respondsToSelector:@selector
(mediapipeGraph:didOutputPixelBuffer:fromStream:timestamp:)]) {
[wrapper.delegate mediapipeGraph:wrapper
didOutputPixelBuffer:pixelBuffer
fromStream:streamName
timestamp:packet.Timestamp()];
} else if ([wrapper.delegate
respondsToSelector:@selector
(mediapipeGraph:didOutputPixelBuffer:fromStream:)]) {
[wrapper.delegate mediapipeGraph:wrapper
didOutputPixelBuffer:pixelBuffer
fromStream:streamName];
}
#endif // MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
} else {
_GTMDevLog(@"unsupported packet type");
}
wrapper->_framesInFlight--;
}
}
- (void)setHeaderPacket:(const mediapipe::Packet&)packet forStream:(const std::string&)streamName {
_GTMDevAssert(!_started, @"%@ must be called before the graph is started",
NSStringFromSelector(_cmd));
_streamHeaders[streamName] = packet;
}
- (void)setSidePacket:(const mediapipe::Packet&)packet named:(const std::string&)name {
_GTMDevAssert(!_started, @"%@ must be called before the graph is started",
NSStringFromSelector(_cmd));
_inputSidePackets[name] = packet;
}
- (void)addSidePackets:(const std::map<std::string, mediapipe::Packet>&)extraSidePackets {
_GTMDevAssert(!_started, @"%@ must be called before the graph is started",
NSStringFromSelector(_cmd));
_inputSidePackets.insert(extraSidePackets.begin(), extraSidePackets.end());
}
- (BOOL)startWithError:(NSError**)error {
::mediapipe::Status status = _graph->Initialize(_config);
if (status.ok()) {
status = _graph->StartRun(_inputSidePackets, _streamHeaders);
if (status.ok()) {
_started = YES;
return YES;
}
}
if (error) {
*error = [NSError gus_errorWithStatus:status];
}
return NO;
}
- (void)cancel {
_graph->Cancel();
}
- (BOOL)hasInputStream:(const std::string&)inputName {
return _graph->HasInputStream(inputName);
}
- (BOOL)closeInputStream:(const std::string&)inputName error:(NSError**)error {
::mediapipe::Status status = _graph->CloseInputStream(inputName);
if (!status.ok() && error) *error = [NSError gus_errorWithStatus:status];
return status.ok();
}
- (BOOL)closeAllInputStreamsWithError:(NSError**)error {
::mediapipe::Status status = _graph->CloseAllInputStreams();
if (!status.ok() && error) *error = [NSError gus_errorWithStatus:status];
return status.ok();
}
- (BOOL)waitUntilDoneWithError:(NSError**)error {
// Since this method blocks with no timeout, it should not be called in the main thread in
// an app. However, it's fine to allow that in a test.
// TODO: is this too heavy-handed? Maybe a warning would be fine.
_GTMDevAssert(![NSThread isMainThread] || (NSClassFromString(@"XCTest")),
@"waitUntilDoneWithError: should not be called on the main thread");
::mediapipe::Status status = _graph->WaitUntilDone();
_started = NO;
if (!status.ok() && error) *error = [NSError gus_errorWithStatus:status];
return status.ok();
}
- (BOOL)waitUntilIdleWithError:(NSError**)error {
::mediapipe::Status status = _graph->WaitUntilIdle();
if (!status.ok() && error) *error = [NSError gus_errorWithStatus:status];
return status.ok();
}
- (BOOL)movePacket:(mediapipe::Packet&&)packet
intoStream:(const std::string&)streamName
error:(NSError**)error {
::mediapipe::Status status = _graph->AddPacketToInputStream(streamName, std::move(packet));
if (!status.ok() && error) *error = [NSError gus_errorWithStatus:status];
return status.ok();
}
- (BOOL)sendPacket:(const mediapipe::Packet&)packet
intoStream:(const std::string&)streamName
error:(NSError**)error {
::mediapipe::Status status = _graph->AddPacketToInputStream(streamName, packet);
if (!status.ok() && error) *error = [NSError gus_errorWithStatus:status];
return status.ok();
}
- (BOOL)setMaxQueueSize:(int)maxQueueSize
forStream:(const std::string&)streamName
error:(NSError**)error {
::mediapipe::Status status = _graph->SetInputStreamMaxQueueSize(streamName, maxQueueSize);
if (!status.ok() && error) *error = [NSError gus_errorWithStatus:status];
return status.ok();
}
- (mediapipe::Packet)packetWithPixelBuffer:(CVPixelBufferRef)imageBuffer
packetType:(MediaPipePacketType)packetType {
mediapipe::Packet packet;
if (packetType == MediaPipePacketImageFrame || packetType == MediaPipePacketImageFrameBGRANoSwap) {
auto frame = CreateImageFrameForCVPixelBuffer(
imageBuffer, /* canOverwrite = */ false,
/* bgrAsRgb = */ packetType == MediaPipePacketImageFrameBGRANoSwap);
packet = mediapipe::Adopt(frame.release());
#if MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
} else if (packetType == MediaPipePacketPixelBuffer) {
packet = mediapipe::MakePacket<mediapipe::GpuBuffer>(imageBuffer);
#endif // MEDIAPIPE_GPU_BUFFER_USE_CV_PIXEL_BUFFER
} else {
_GTMDevLog(@"unsupported packet type: %d", packetType);
}
return packet;
}
- (BOOL)sendPixelBuffer:(CVPixelBufferRef)imageBuffer
intoStream:(const std::string&)inputName
packetType:(MediaPipePacketType)packetType
timestamp:(const mediapipe::Timestamp&)timestamp
allowOverwrite:(BOOL)allowOverwrite {
if (_maxFramesInFlight && _framesInFlight >= _maxFramesInFlight) return NO;
mediapipe::Packet packet = [self packetWithPixelBuffer:imageBuffer packetType:packetType];
NSError* error;
BOOL success;
if (allowOverwrite) {
packet = std::move(packet).At(timestamp);
success = [self movePacket:std::move(packet)
intoStream:inputName
error:&error];
} else {
success = [self sendPacket:packet.At(timestamp)
intoStream:inputName
error:&error];
}
if (success) _framesInFlight++;
else _GTMDevLog(@"failed to send packet: %@", error);
return success;
}
- (BOOL)sendPixelBuffer:(CVPixelBufferRef)imageBuffer
intoStream:(const std::string&)inputName
packetType:(MediaPipePacketType)packetType
timestamp:(const mediapipe::Timestamp&)timestamp {
return [self sendPixelBuffer:imageBuffer
intoStream:inputName
packetType:packetType
timestamp:timestamp
allowOverwrite:NO];
}
- (BOOL)sendPixelBuffer:(CVPixelBufferRef)imageBuffer
intoStream:(const std::string&)inputName
packetType:(MediaPipePacketType)packetType {
_GTMDevAssert(_frameTimestamp < mediapipe::Timestamp::Done(),
@"Trying to send frame after stream is done.");
if (_frameTimestamp < mediapipe::Timestamp::Min()) {
_frameTimestamp = mediapipe::Timestamp::Min();
} else {
_frameTimestamp++;
}
return [self sendPixelBuffer:imageBuffer
intoStream:inputName
packetType:packetType
timestamp:_frameTimestamp];
}
- (void)debugPrintGlInfo {
std::shared_ptr<mediapipe::GpuResources> gpu_resources = _graph->GetGpuResources();
if (!gpu_resources) {
NSLog(@"GPU not set up.");
return;
}
NSString* extensionString;
(void)gpu_resources->gl_context()->Run([&extensionString]{
extensionString = [NSString stringWithUTF8String:(char*)glGetString(GL_EXTENSIONS)];
return ::mediapipe::OkStatus();
});
NSArray* extensions = [extensionString componentsSeparatedByCharactersInSet:
[NSCharacterSet whitespaceCharacterSet]];
for (NSString* oneExtension in extensions)
NSLog(@"%@", oneExtension);
}
@end
+141
View File
@@ -0,0 +1,141 @@
// Copyright 2019 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 <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#import "mediapipe/objc/MPPGraph.h"
#import "mediapipe/objc/NSError+util_status.h"
#import "mediapipe/objc/util.h"
/// This XCTestCase subclass provides common convenience methods for testing
/// with MPPGraph.
@interface MPPGraphTestBase : XCTestCase <MPPGraphDelegate> {
/// This block is used to respond to mediapipeGraph:didOutputPixelBuffer:fromStream:.
/// runGraph:withPixelBuffer:packetType: uses this internally, but you can reuse it
/// if you need to run a graph directly and want a MPPGraphTestBase object to
/// act as the delegate.
void (^_pixelBufferOutputBlock)(MPPGraph* graph, CVPixelBufferRef imageBuffer,
const std::string& streamName);
/// This block is used to respond to mediapipeGraph:didOutputPacket:fromStream:.
/// You can use it if you need to run a graph directly and want a MPPGraphTestBase
/// object to act as the delegate.
void (^_packetOutputBlock)(MPPGraph* graph, const mediapipe::Packet& packet,
const std::string& streamName);
}
/// Runs a single frame through a simple graph. The graph is expected to have an
/// input stream named "input_frames" and an output stream named
/// "output_frames". This function runs the graph, sends inputBuffer into
/// input_frames (at timestamp=1), receives an output buffer from output_frames,
/// completes the run, and returns the output frame.
- (CVPixelBufferRef)runGraph:(MPPGraph*)graph
withPixelBuffer:(CVPixelBufferRef)inputBuffer
packetType:(MediaPipePacketType)inputPacketType;
/// Runs a simple graph, providing a single frame to zero or more inputs. Input images are wrapped
/// in packets each with timestamp mediapipe::Timestamp(1). Those packets are added to the
/// designated streams (named by the keys of withInputPixelBuffers). When a packet arrives on the
/// output stream, the graph run is done and the output frame is returned.
- (CVPixelBufferRef)runGraph:(MPPGraph*)graph
withInputPixelBuffers:
(const std::unordered_map<std::string, CFHolder<CVPixelBufferRef>>&)inputBuffers
outputStream:(const std::string&)output
packetType:(MediaPipePacketType)inputPacketType;
/// Loads a data file from the test bundle.
- (NSData*)testDataNamed:(NSString*)name extension:(NSString*)extension;
/// Loads an image from the test bundle.
- (UIImage*)testImageNamed:(NSString*)name extension:(NSString*)extension;
/// Loads an image from the test bundle in subpath.
- (UIImage*)testImageNamed:(NSString*)name
extension:(NSString*)extension
subdirectory:(NSString*)subdirectory;
/// Compares two pixel buffers for strict equality.
/// Returns true iff the two buffers have the same size, format, and pixel data.
- (BOOL)pixelBuffer:(CVPixelBufferRef)a isEqualTo:(CVPixelBufferRef)b;
/// Compares two pixel buffers with some leniency.
/// Returns true iff the two buffers have the same size and format, and:
/// - the difference between each pixel of A and the corresponding pixel of B does
/// not exceed maxLocalDiff, and
/// - the average difference between corresponding pixels of A and B does not
/// exceed maxAvgDiff.
- (BOOL)pixelBuffer:(CVPixelBufferRef)a
isCloseTo:(CVPixelBufferRef)b
maxLocalDifference:(int)maxLocalDiff
maxAverageDifference:(float)maxAvgDiff;
/// Utility function for making a copy of a pixel buffer with a different pixel
/// format.
- (CVPixelBufferRef)convertPixelBuffer:(CVPixelBufferRef)input toPixelFormat:(OSType)pixelFormat;
/// Makes a scaled copy of a BGRA pixel buffer.
- (CVPixelBufferRef)scaleBGRAPixelBuffer:(CVPixelBufferRef)input toSize:(CGSize)size;
/// Utility function for transforming a pixel buffer.
/// It creates a new pixel buffer with the same dimensions as the original, in the
/// desired pixel format, and invokes a block with the input and output buffers.
/// The buffers are locked before the block and unlocked after, so the block can read
/// from the input buffer and write to the output buffer without further preparation.
- (CVPixelBufferRef)transformPixelBuffer:(CVPixelBufferRef)input
outputPixelFormat:(OSType)pixelFormat
transformation:(void (^)(CVPixelBufferRef input,
CVPixelBufferRef output))transformation;
/// Computes a difference image from two input images. Useful for debugging.
- (UIImage*)differenceOfImage:(UIImage*)inputA image:(UIImage*)inputB;
/// Tests a graph by sending in the provided input pixel buffer and comparing the
/// output with the provided expected output. Uses runGraph:withPixelBuffer:packetType:
/// internally, so the streams are supposed to be named "input_frames" and "output_frames".
/// The actual and expected outputs are compared fuzzily.
- (void)testGraph:(MPPGraph*)graph
input:(CVPixelBufferRef)inputBuffer
expectedOutput:(CVPixelBufferRef)expectedBuffer;
/// Tests a graph by sending the provided image files as pixelBuffer inputs to the
/// corresponding streams, and comparing the single frame output by the given output stream
/// with the contents of the given output file.
/// @param config Graph config.
/// @param fileInputs Dictionary mapping input stream names to image file paths.
/// @param packetInputs Map of input stream names to additional input packets.
/// @param sidePackets Map of input side packet stream names to packets.
/// @param outputStream Name of the output stream where the output is produced.
/// @param expectedPath Path to an image file containing the expected output.
/// @param maxAverageDifference The maximum allowable average pixel difference
/// between the
/// expected output and computed output.
/// TODO: Use NSDictionary instead of std::map for sidePackets.
- (void)testGraphConfig:(const mediapipe::CalculatorGraphConfig&)config
inputStreamsAndFiles:(NSDictionary<NSString*, NSString*>*)fileInputs
inputStreamsAndPackets:(const std::map<std::string, mediapipe::Packet>&)packetInputs
sidePackets:(std::map<std::string, mediapipe::Packet>)sidePackets
timestamp:(mediapipe::Timestamp)timestamp
outputStream:(NSString*)outputStream
expectedOutputFile:(NSString*)expectedPath
maxAverageDifference:(float)maxAverageDifference;
/// Calls the above testGraphConfig: method with a default maxAverageDifference
/// of 1.f and timestamp of 1.
- (void)testGraphConfig:(const mediapipe::CalculatorGraphConfig&)config
inputStreamsAndFiles:(NSDictionary<NSString*, NSString*>*)inputs
outputStream:(NSString*)outputStream
expectedOutputFile:(NSString*)expectedPath;
@end
+446
View File
@@ -0,0 +1,446 @@
// Copyright 2019 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/objc/MPPGraphTestBase.h"
#import "mediapipe/objc/Weakify.h"
#include "absl/memory/memory.h"
static UIImage* UIImageWithPixelBuffer(CVPixelBufferRef pixelBuffer) {
CFHolder<CGImageRef> cgImage;
::mediapipe::Status status = CreateCGImageFromCVPixelBuffer(pixelBuffer, &cgImage);
if (!status.ok()) {
return nil;
}
UIImage *uiImage = [UIImage imageWithCGImage:*cgImage
scale:1.0
orientation:UIImageOrientationUp];
return uiImage;
}
static void EnsureOutputDirFor(NSString *outputFile) {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
BOOL result = [fileManager createDirectoryAtPath:[outputFile stringByDeletingLastPathComponent]
withIntermediateDirectories:YES
attributes:nil
error:&error];
// TODO: Log the error for clarity. The file-write will fail later
// but it would be nice to see this error. However, 'error' is still testing
// false and result is true even on an unwritable path-- not sure what's up.
}
@implementation MPPGraphTestBase
- (NSData*)testDataNamed:(NSString*)name extension:(NSString*)extension {
NSBundle* testBundle = [NSBundle bundleForClass:[self class]];
NSURL* resourceURL = [testBundle URLForResource:name withExtension:extension];
XCTAssertNotNil(resourceURL,
@"Unable to find data with name: %@. Did you add it to your resources?", name);
NSError* error;
NSData* data = [NSData dataWithContentsOfURL:resourceURL options:0 error:&error];
XCTAssertNotNil(data, @"%@: %@", resourceURL.path, error);
return data;
}
- (UIImage*)testImageNamed:(NSString*)name extension:(NSString*)extension {
return [self testImageNamed:name extension:extension subdirectory:nil];
}
- (UIImage*)testImageNamed:(NSString*)name
extension:(NSString*)extension
subdirectory:(NSString *)subdirectory {
// imageNamed does not work in our test bundle
NSBundle* testBundle = [NSBundle bundleForClass:[self class]];
NSURL* imageURL = subdirectory ?
[testBundle URLForResource:name withExtension:extension subdirectory:subdirectory] :
[testBundle URLForResource:name withExtension:extension];
XCTAssertNotNil(imageURL,
@"Unable to find image with name: %@. Did you add it to your resources?", name);
NSError* error;
NSData* imageData = [NSData dataWithContentsOfURL:imageURL options:0 error:&error];
UIImage* image = [UIImage imageWithData:imageData];
XCTAssertNotNil(image, @"%@: %@", imageURL.path, error);
return image;
}
- (CVPixelBufferRef)runGraph:(MPPGraph*)graph
withInputPixelBuffers:
(const std::unordered_map<std::string, CFHolder<CVPixelBufferRef>>&)inputBuffers
inputPackets:(const std::map<std::string, mediapipe::Packet>&)inputPackets
timestamp:(mediapipe::Timestamp)timestamp
outputStream:(const std::string&)outputStream
packetType:(MediaPipePacketType)inputPacketType {
__block CVPixelBufferRef output;
graph.delegate = self;
// The XCTAssert macros contain references to self, which causes a retain cycle,
// since the block retains self and self retains the block. The cycle is broken
// at the end of this method, with _pixelBufferOutputBlock = nil, but Clang does
// not realize that and outputs a warning. WEAKIFY and STRONGIFY, though not
// strictly necessary, are used here to avoid the warning.
WEAKIFY(self);
if (!_pixelBufferOutputBlock) {
XCTestExpectation* outputReceived = [self expectationWithDescription:@"output received"];
_pixelBufferOutputBlock = ^(MPPGraph* outputGraph, CVPixelBufferRef outputBuffer,
const std::string& outputStreamName) {
STRONGIFY(self);
XCTAssertEqualObjects(outputGraph, graph);
XCTAssertEqual(outputStreamName, outputStream);
CFRetain(outputBuffer);
output = outputBuffer;
[outputReceived fulfill];
};
}
NSError *error;
BOOL success = [graph startWithError:&error];
// Normally we continue after failures, but there is no sense in waiting for an
// output if the graph didn't even start.
BOOL savedContinue = self.continueAfterFailure;
self.continueAfterFailure = NO;
XCTAssert(success, @"%@", error.localizedDescription);
self.continueAfterFailure = savedContinue;
for (const auto& stream_buffer : inputBuffers) {
[graph sendPixelBuffer:*stream_buffer.second
intoStream:stream_buffer.first
packetType:inputPacketType
timestamp:timestamp];
success = [graph closeInputStream:stream_buffer.first error:&error];
XCTAssert(success, @"%@", error.localizedDescription);
}
for (const auto& stream_packet : inputPackets) {
[graph sendPacket:stream_packet.second
intoStream:stream_packet.first
error:&error];
success = [graph closeInputStream:stream_packet.first error:&error];
XCTAssert(success, @"%@", error.localizedDescription);
}
XCTestExpectation* graphDone = [self expectationWithDescription:@"graph done"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error;
BOOL success = [graph waitUntilDoneWithError:&error];
XCTAssert(success, @"%@", error.localizedDescription);
[graphDone fulfill];
});
[self waitForExpectationsWithTimeout:8.0 handler:NULL];
_pixelBufferOutputBlock = nil;
return output;
}
- (CVPixelBufferRef)runGraph:(MPPGraph*)graph
withPixelBuffer:(CVPixelBufferRef)inputBuffer
packetType:(MediaPipePacketType)inputPacketType {
return [self runGraph:graph
withInputPixelBuffers:{{"input_frames", MakeCFHolder(inputBuffer)}}
inputPackets:{}
timestamp:mediapipe::Timestamp(1)
outputStream:"output_frames"
packetType:inputPacketType];
}
- (CVPixelBufferRef)runGraph:(MPPGraph*)graph
withInputPixelBuffers:
(const std::unordered_map<std::string, CFHolder<CVPixelBufferRef>>&)inputBuffers
outputStream:(const std::string&)output
packetType:(MediaPipePacketType)inputPacketType {
return [self runGraph:graph
withInputPixelBuffers:inputBuffers
inputPackets:{}
timestamp:mediapipe::Timestamp(1)
outputStream:output
packetType:inputPacketType];
}
// By using a block to handle the delegate message, we can change the
// implementation for each test.
- (void)mediapipeGraph:(MPPGraph*)graph
didOutputPixelBuffer:(CVPixelBufferRef)imageBuffer
fromStream:(const std::string&)streamName {
_pixelBufferOutputBlock(graph, imageBuffer, streamName);
}
- (void)mediapipeGraph:(MPPGraph*)graph
didOutputPacket:(const mediapipe::Packet&)packet
fromStream:(const std::string&)streamName {
_packetOutputBlock(graph, packet, streamName);
}
- (BOOL)pixelBuffer:(CVPixelBufferRef)a isEqualTo:(CVPixelBufferRef)b {
return [self pixelBuffer:a isCloseTo:b maxLocalDifference:0 maxAverageDifference:0];
}
- (BOOL)pixelBuffer:(CVPixelBufferRef)a isCloseTo:(CVPixelBufferRef)b
maxLocalDifference:(int)maxLocalDiff maxAverageDifference:(float)maxAvgDiff {
size_t aBytesPerRow = CVPixelBufferGetBytesPerRow(a);
size_t aWidth = CVPixelBufferGetWidth(a);
size_t aHeight = CVPixelBufferGetHeight(a);
OSType aPixelFormat = CVPixelBufferGetPixelFormatType(a);
XCTAssertFalse(CVPixelBufferIsPlanar(a), @"planar buffers not supported");
size_t bBytesPerRow = CVPixelBufferGetBytesPerRow(b);
size_t bWidth = CVPixelBufferGetWidth(b);
size_t bHeight = CVPixelBufferGetHeight(b);
OSType bPixelFormat = CVPixelBufferGetPixelFormatType(b);
XCTAssertFalse(CVPixelBufferIsPlanar(b), @"planar buffers not supported");
if (aPixelFormat != bPixelFormat ||
aWidth != bWidth ||
aHeight != bHeight) return NO;
size_t bytesPerPixel; // is there a generic way to get this from a pixel buffer?
switch (aPixelFormat) {
case kCVPixelFormatType_32BGRA:
bytesPerPixel = 4;
break;
case kCVPixelFormatType_OneComponent8:
bytesPerPixel = 1;
break;
default:
XCTFail(@"unsupported pixel format");
}
CVReturn err;
err = CVPixelBufferLockBaseAddress(a, kCVPixelBufferLock_ReadOnly);
XCTAssertEqual(err, kCVReturnSuccess);
err = CVPixelBufferLockBaseAddress(b, kCVPixelBufferLock_ReadOnly);
XCTAssertEqual(err, kCVReturnSuccess);
const uint8_t* aData = static_cast<uint8_t*>(CVPixelBufferGetBaseAddress(a));
const uint8_t* bData = static_cast<uint8_t*>(CVPixelBufferGetBaseAddress(b));
// Let's not assume identical bytesPerRow. Also, the padding may not be equal
// even if bytesPerRow match.
size_t usedRowWidth = aWidth * bytesPerPixel;
BOOL equal = YES;
float averageDiff = 0;
float count = 0;
for (int i = aHeight; i > 0 && equal; --i) {
if (maxLocalDiff == 0) {
// If we can, use memcmp for speed.
equal = memcmp(aData, bData, usedRowWidth) == 0;
} else {
for (int j = 0; j < usedRowWidth; j++) {
int diff = abs(aData[j] - bData[j]);
if (diff > maxLocalDiff) {
equal = NO;
break;
}
// We use Welford's algorithm for computing a sample mean. This has better
// numerical stability than the naive method, as noted in TAoCP. Not that it
// particularly matters here.
// Welford: http://www.jstor.org/stable/1266577
// Knuth: The Art of Computer Programming Vol 2, section 4.2.2
averageDiff += (diff - averageDiff) / ++count;
}
}
aData += aBytesPerRow;
bData += bBytesPerRow;
}
if (averageDiff > maxAvgDiff) equal = NO;
err = CVPixelBufferUnlockBaseAddress(b, kCVPixelBufferLock_ReadOnly);
XCTAssertEqual(err, kCVReturnSuccess);
err = CVPixelBufferUnlockBaseAddress(a, kCVPixelBufferLock_ReadOnly);
XCTAssertEqual(err, kCVReturnSuccess);
return equal;
}
- (CVPixelBufferRef)convertPixelBuffer:(CVPixelBufferRef)input
toPixelFormat:(OSType)pixelFormat {
size_t width = CVPixelBufferGetWidth(input);
size_t height = CVPixelBufferGetHeight(input);
CVPixelBufferRef output;
CVReturn status = CVPixelBufferCreate(
kCFAllocatorDefault, width, height, pixelFormat,
GetCVPixelBufferAttributesForGlCompatibility(), &output);
XCTAssertEqual(status, kCVReturnSuccess);
status = CVPixelBufferLockBaseAddress(input, kCVPixelBufferLock_ReadOnly);
XCTAssertEqual(status, kCVReturnSuccess);
status = CVPixelBufferLockBaseAddress(output, 0);
XCTAssertEqual(status, kCVReturnSuccess);
status = vImageConvertCVPixelBuffers(input, output);
XCTAssertEqual(status, kvImageNoError);
status = CVPixelBufferUnlockBaseAddress(output, 0);
XCTAssertEqual(status, kCVReturnSuccess);
status = CVPixelBufferUnlockBaseAddress(input, kCVPixelBufferLock_ReadOnly);
XCTAssertEqual(status, kCVReturnSuccess);
return output;
}
- (CVPixelBufferRef)scaleBGRAPixelBuffer:(CVPixelBufferRef)input
toSize:(CGSize)size {
CVPixelBufferRef output;
CVReturn status = CVPixelBufferCreate(
kCFAllocatorDefault, size.width, size.height, kCVPixelFormatType_32BGRA,
GetCVPixelBufferAttributesForGlCompatibility(), &output);
XCTAssertEqual(status, kCVReturnSuccess);
status = CVPixelBufferLockBaseAddress(input, kCVPixelBufferLock_ReadOnly);
XCTAssertEqual(status, kCVReturnSuccess);
status = CVPixelBufferLockBaseAddress(output, 0);
XCTAssertEqual(status, kCVReturnSuccess);
vImage_Buffer src = vImageForCVPixelBuffer(input);
vImage_Buffer dst = vImageForCVPixelBuffer(output);
status = vImageScale_ARGB8888(&src, &dst, NULL, kvImageNoFlags);
XCTAssertEqual(status, kvImageNoError);
status = CVPixelBufferUnlockBaseAddress(output, 0);
XCTAssertEqual(status, kCVReturnSuccess);
status = CVPixelBufferUnlockBaseAddress(input, kCVPixelBufferLock_ReadOnly);
XCTAssertEqual(status, kCVReturnSuccess);
return output;
}
- (CVPixelBufferRef)transformPixelBuffer:(CVPixelBufferRef)input
outputPixelFormat:(OSType)pixelFormat
transformation:(void(^)(CVPixelBufferRef input,
CVPixelBufferRef output))transformation {
size_t width = CVPixelBufferGetWidth(input);
size_t height = CVPixelBufferGetHeight(input);
CVPixelBufferRef output;
CVReturn status = CVPixelBufferCreate(
kCFAllocatorDefault, width, height,
pixelFormat, NULL, &output);
XCTAssertEqual(status, kCVReturnSuccess);
status = CVPixelBufferLockBaseAddress(input, kCVPixelBufferLock_ReadOnly);
XCTAssertEqual(status, kCVReturnSuccess);
status = CVPixelBufferLockBaseAddress(output, 0);
XCTAssertEqual(status, kCVReturnSuccess);
transformation(input, output);
status = CVPixelBufferUnlockBaseAddress(output, 0);
XCTAssertEqual(status, kCVReturnSuccess);
status = CVPixelBufferUnlockBaseAddress(input, kCVPixelBufferLock_ReadOnly);
XCTAssertEqual(status, kCVReturnSuccess);
return output;
}
- (UIImage*)differenceOfImage:(UIImage*)inputA image:(UIImage*)inputB {
UIGraphicsBeginImageContextWithOptions(inputA.size, YES, 1.0);
CGRect imageRect = CGRectMake(0, 0, inputA.size.width, inputA.size.height);
[inputA drawInRect:imageRect blendMode:kCGBlendModeNormal alpha:1.0];
[inputB drawInRect:imageRect blendMode:kCGBlendModeDifference alpha:1.0];
UIImage *differenceImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return differenceImage;
}
- (void)testGraph:(MPPGraph*)graph
input:(CVPixelBufferRef)inputBuffer
expectedOutput:(CVPixelBufferRef)expectedBuffer
{
CVPixelBufferRef outputBuffer = [self runGraph:graph
withPixelBuffer:inputBuffer
packetType:MediaPipePacketPixelBuffer];
#if DEBUG
// Xcode can display UIImage objects right in the debugger. It is handy to
// have these variables defined if the test fails.
UIImage* output = UIImageWithPixelBuffer(outputBuffer);
XCTAssertNotNil(output);
UIImage* expected = UIImageWithPixelBuffer(expectedBuffer);
XCTAssertNotNil(expected);
UIImage* diff = [self differenceOfImage:output image:expected];
(void)diff; // Suppress unused variable warning.
#endif
XCTAssert([self pixelBuffer:outputBuffer isCloseTo:expectedBuffer
maxLocalDifference:INT_MAX maxAverageDifference:1]);
CFRelease(outputBuffer);
}
- (void)testGraphConfig:(const mediapipe::CalculatorGraphConfig&)config
inputStreamsAndFiles:(NSDictionary<NSString*, NSString*>*)inputs
outputStream:(NSString*)outputStream
expectedOutputFile:(NSString*)expectedPath {
[self testGraphConfig:config
inputStreamsAndFiles:inputs
inputStreamsAndPackets:{}
sidePackets:{}
timestamp:mediapipe::Timestamp(1)
outputStream:outputStream
expectedOutputFile:expectedPath
maxAverageDifference:1.f];
}
- (void)testGraphConfig:(const mediapipe::CalculatorGraphConfig&)config
inputStreamsAndFiles:(NSDictionary<NSString*, NSString*>*)fileInputs
inputStreamsAndPackets:(const std::map<std::string, mediapipe::Packet>&)packetInputs
sidePackets:(std::map<std::string, mediapipe::Packet>)sidePackets
timestamp:(mediapipe::Timestamp)timestamp
outputStream:(NSString*)outputStream
expectedOutputFile:(NSString*)expectedPath
maxAverageDifference:(float)maxAverageDifference {
NSBundle* testBundle = [NSBundle bundleForClass:[self class]];
chdir([testBundle.resourcePath fileSystemRepresentation]);
MPPGraph* graph = [[MPPGraph alloc] initWithGraphConfig:config];
[graph addSidePackets:sidePackets];
[graph addFrameOutputStream:outputStream.UTF8String
outputPacketType:MediaPipePacketPixelBuffer];
std::unordered_map<std::string, CFHolder<CVPixelBufferRef>> inputBuffers;
for (NSString* inputStream in fileInputs) {
UIImage* inputImage = [self testImageNamed:fileInputs[inputStream] extension:nil];
XCTAssertNotNil(inputImage);
::mediapipe::Status status =
CreateCVPixelBufferFromCGImage(inputImage.CGImage, &inputBuffers[inputStream.UTF8String]);
XCTAssert(status.ok());
}
UIImage* expectedImage = [self testImageNamed:expectedPath extension:nil];
XCTAssertNotNil(expectedImage);
CFHolder<CVPixelBufferRef> expectedBuffer;
::mediapipe::Status status =
CreateCVPixelBufferFromCGImage(expectedImage.CGImage, &expectedBuffer);
XCTAssert(status.ok());
CVPixelBufferRef outputBuffer = [self runGraph:graph
withInputPixelBuffers:inputBuffers
inputPackets:packetInputs
timestamp:timestamp
outputStream:outputStream.UTF8String
packetType:MediaPipePacketPixelBuffer];
UIImage* output = UIImageWithPixelBuffer(outputBuffer);
XCTAssertNotNil(output);
UIImage* expected = UIImageWithPixelBuffer(*expectedBuffer);
XCTAssertNotNil(expected);
UIImage* diff = [self differenceOfImage:output image:expected];
XCTAssert([self pixelBuffer:outputBuffer isCloseTo:*expectedBuffer
maxLocalDifference:INT_MAX maxAverageDifference:maxAverageDifference]);
CFRelease(outputBuffer);
}
@end
+342
View File
@@ -0,0 +1,342 @@
// Copyright 2019 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 <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#include "absl/memory/memory.h"
#import "mediapipe/objc/MPPGraph.h"
#import "mediapipe/objc/MPPGraphTestBase.h"
#import "mediapipe/objc/NSError+util_status.h"
#import "mediapipe/objc/Weakify.h"
#import "mediapipe/objc/util.h"
static const char* kExpectedError = "Expected error.";
namespace mediapipe {
class GrayscaleCalculator : public Calculator {
public:
static ::mediapipe::Status FillExpectations(const CalculatorOptions& options,
PacketTypeSet* inputs, PacketTypeSet* outputs,
PacketTypeSet* input_side_packets) {
inputs->Index(0).Set<ImageFrame>();
outputs->Index(0).Set<ImageFrame>();
return ::util::OkStatus();
}
::mediapipe::Status Process() final {
const auto& input = Input()->Get<ImageFrame>();
int w = input.Width();
int h = input.Height();
auto output = absl::make_unique<mediapipe::ImageFrame>(ImageFormat::GRAY8, w, h);
vImage_Buffer src = vImageForImageFrame(input);
vImage_Buffer dst = vImageForImageFrame(*output);
vImage_Error vErr = vImageRGBAToGray(&src, &dst);
NSCAssert(vErr == kvImageNoError, @"vImageRGBAToGray failed: %zd", vErr);
Output()->Add(output.release(), InputTimestamp());
return ::util::OkStatus();
}
};
REGISTER_CALCULATOR(GrayscaleCalculator);
// For testing that video header is present. Open() will have a failure status
// if the video header is not present in the input stream.
class VideoHeaderCalculator : public Calculator {
public:
static ::mediapipe::Status FillExpectations(const CalculatorOptions& options,
PacketTypeSet* inputs, PacketTypeSet* outputs,
PacketTypeSet* input_side_packets) {
inputs->Index(0).Set<ImageFrame>();
outputs->Index(0).Set<ImageFrame>();
return ::util::OkStatus();
}
::mediapipe::Status Open() final {
if (Input()->Header().IsEmpty()) {
return ::util::UnknownError("No video header present.");
}
return ::util::OkStatus();
}
::mediapipe::Status Process() final {
Output()->AddPacket(Input()->Value());
return ::util::OkStatus();
}
};
REGISTER_CALCULATOR(VideoHeaderCalculator);
class ErrorCalculator : public Calculator {
public:
static ::mediapipe::Status FillExpectations(const CalculatorOptions& options,
PacketTypeSet* inputs, PacketTypeSet* outputs,
PacketTypeSet* input_side_packets) {
inputs->Index(0).SetAny();
outputs->Index(0).SetSameAs(&inputs->Index(0));
return ::util::OkStatus();
}
::mediapipe::Status Process() final {
return ::mediapipe::Status(absl::StatusCode::kUnknown, kExpectedError);
}
};
REGISTER_CALCULATOR(ErrorCalculator);
} // namespace mediapipe
@interface MPPGraphTests : MPPGraphTestBase{
UIImage* _sourceImage;
MPPGraph* _graph;
}
@end
@implementation MPPGraphTests
- (void)setUp {
[super setUp];
_sourceImage = [self testImageNamed:@"googlelogo_color_272x92dp" extension:@"png"];
}
- (void)tearDown {
[super tearDown];
}
- (void)testPassThrough {
mediapipe::CalculatorGraphConfig config;
config.add_input_stream("input_frames");
auto node = config.add_node();
node->set_calculator("PassThroughCalculator");
node->add_input_stream("input_frames");
node->add_output_stream("output_frames");
_graph = [[MPPGraph alloc] initWithGraphConfig:config];
[_graph addFrameOutputStream:"output_frames" outputPacketType:MediaPipePacketPixelBuffer];
CFHolder<CVPixelBufferRef> inputBuffer;
::mediapipe::Status status = CreateCVPixelBufferFromCGImage(_sourceImage.CGImage, &inputBuffer);
XCTAssert(status.ok());
CVPixelBufferRef outputBuffer = [self runGraph:_graph
withPixelBuffer:*inputBuffer
packetType:MediaPipePacketPixelBuffer];
XCTAssert([self pixelBuffer:outputBuffer isEqualTo:*inputBuffer]);
}
- (UIImage*)grayImage:(UIImage*)inputImage {
UIGraphicsBeginImageContextWithOptions(inputImage.size, YES, 1.0);
CGRect imageRect = CGRectMake(0, 0, inputImage.size.width, inputImage.size.height);
// Draw the image with the luminosity blend mode.
// On top of a white background, this will give a black and white image.
[inputImage drawInRect:imageRect blendMode:kCGBlendModeLuminosity alpha:1.0];
UIImage *filteredImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return filteredImage;
}
- (void)testMultipleOutputs {
mediapipe::CalculatorGraphConfig config;
config.add_input_stream("input_frames");
auto passThroughNode = config.add_node();
passThroughNode->set_calculator("PassThroughCalculator");
passThroughNode->add_input_stream("input_frames");
passThroughNode->add_output_stream("pass_frames");
auto grayNode = config.add_node();
grayNode->set_calculator("GrayscaleCalculator");
grayNode->add_input_stream("input_frames");
grayNode->add_output_stream("gray_frames");
_graph = [[MPPGraph alloc] initWithGraphConfig:config];
[_graph addFrameOutputStream:"pass_frames" outputPacketType:MediaPipePacketImageFrame];
[_graph addFrameOutputStream:"gray_frames" outputPacketType:MediaPipePacketImageFrame];
CFHolder<CVPixelBufferRef> inputBuffer;
::mediapipe::Status status = CreateCVPixelBufferFromCGImage(_sourceImage.CGImage, &inputBuffer);
XCTAssert(status.ok());
WEAKIFY(self);
XCTestExpectation* passFrameReceive =
[self expectationWithDescription:@"pass through output received"];
XCTestExpectation* grayFrameReceive =
[self expectationWithDescription:@"grayscale output received"];
_pixelBufferOutputBlock = ^(MPPGraph* outputGraph, CVPixelBufferRef outputBuffer,
const std::string& outputStreamName) {
STRONGIFY(self);
XCTAssertEqualObjects(outputGraph, self->_graph);
if (outputStreamName == "pass_frames") {
[passFrameReceive fulfill];
} else if (outputStreamName == "gray_frames") {
[grayFrameReceive fulfill];
}
};
[self runGraph:_graph withPixelBuffer:*inputBuffer packetType:MediaPipePacketImageFrame];
}
- (void)testGrayscaleOutput {
// When a calculator outputs a grayscale ImageFrame, it is returned to the
// application as a BGRA pixel buffer. To test it, let's feed a grayscale
// image into the graph and make sure it comes out unscathed.
UIImage* grayImage = [self grayImage:_sourceImage];
mediapipe::CalculatorGraphConfig config;
config.add_input_stream("input_frames");
auto node = config.add_node();
node->set_calculator("GrayscaleCalculator");
node->add_input_stream("input_frames");
node->add_output_stream("output_frames");
_graph = [[MPPGraph alloc] initWithGraphConfig:config];
[_graph addFrameOutputStream:"output_frames" outputPacketType:MediaPipePacketImageFrame];
CFHolder<CVPixelBufferRef> inputBuffer;
::mediapipe::Status status = CreateCVPixelBufferFromCGImage(grayImage.CGImage, &inputBuffer);
XCTAssert(status.ok());
CVPixelBufferRef outputBuffer = [self runGraph:_graph
withPixelBuffer:*inputBuffer
packetType:MediaPipePacketImageFrame];
// We accept a small difference due to gamma correction and whatnot.
XCTAssert([self pixelBuffer:outputBuffer isCloseTo:*inputBuffer
maxLocalDifference:5 maxAverageDifference:FLT_MAX]);
}
- (void)testGraphError {
mediapipe::CalculatorGraphConfig config;
config.add_input_stream("input_frames");
auto node = config.add_node();
node->set_calculator("ErrorCalculator");
node->add_input_stream("input_frames");
node->add_output_stream("output_frames");
CFHolder<CVPixelBufferRef> srcPixelBuffer;
::mediapipe::Status status =
CreateCVPixelBufferFromCGImage(_sourceImage.CGImage, &srcPixelBuffer);
XCTAssert(status.ok());
_graph = [[MPPGraph alloc] initWithGraphConfig:config];
[_graph addFrameOutputStream:"output_frames" outputPacketType:MediaPipePacketImageFrame];
_graph.delegate = self;
XCTAssert([_graph startWithError:nil]);
[_graph sendPixelBuffer:*srcPixelBuffer
intoStream:"input_frames"
packetType:MediaPipePacketImageFrame];
XCTAssert([_graph closeInputStream:"input_frames" error:nil]);
__block NSError* error = nil;
XCTestExpectation* graphDone = [self expectationWithDescription:@"graph done"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
XCTAssertFalse([_graph waitUntilDoneWithError:&error]);
[graphDone fulfill];
});
[self waitForExpectationsWithTimeout:3.0 handler:NULL];
XCTAssertNotNil(error);
status = error.gus_status;
XCTAssertNotEqual(status.error_message().find(kExpectedError), std::string::npos,
@"Missing expected std::string '%s' from error messge '%s'", kExpectedError,
status.error_message().c_str());
}
- (void)testSetStreamHeader {
mediapipe::CalculatorGraphConfig config;
config.add_input_stream("input_frames");
auto node = config.add_node();
node->set_calculator("VideoHeaderCalculator");
node->add_input_stream("input_frames");
node->add_output_stream("output_frames");
_graph = [[MPPGraph alloc] initWithGraphConfig:config];
[_graph addFrameOutputStream:"output_frames" outputPacketType:MediaPipePacketImageFrame];
// We're no longer using video headers, let's just use an int as the header.
auto header_packet = mediapipe::MakePacket<int>(0xDEADBEEF);
[_graph setHeaderPacket:header_packet forStream:"input_frames"];
// Verify that Open() on calculator succeeded.
XCTAssert([_graph startWithError:nil]);
// Tear down graph.
XCTAssert([_graph closeInputStream:"input_frames" error:nil]);
XCTestExpectation* graphDone = [self expectationWithDescription:@"graph done"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
XCTAssert([_graph waitUntilDoneWithError:nil]);
[graphDone fulfill];
});
[self waitForExpectationsWithTimeout:3.0 handler:NULL];
}
- (void)testGraphIsDeallocated {
mediapipe::CalculatorGraphConfig config;
config.add_input_stream("input_frames");
auto node = config.add_node();
node->set_calculator("PassThroughCalculator");
node->add_input_stream("input_frames");
node->add_output_stream("output_frames");
_graph = [[MPPGraph alloc] initWithGraphConfig:config];
[_graph addFrameOutputStream:"output_frames" outputPacketType:MediaPipePacketPixelBuffer];
CFHolder<CVPixelBufferRef> inputBuffer;
::mediapipe::Status status = CreateCVPixelBufferFromCGImage(_sourceImage.CGImage, &inputBuffer);
XCTAssert(status.ok());
CVPixelBufferRef outputBuffer = [self runGraph:_graph
withPixelBuffer:*inputBuffer
packetType:MediaPipePacketPixelBuffer];
__weak MPPGraph* weakGraph = _graph;
_graph = nil;
XCTAssertNil(weakGraph);
}
- (void)testRawPacketOutput {
mediapipe::CalculatorGraphConfig config;
config.add_input_stream("input_ints");
auto node = config.add_node();
node->set_calculator("PassThroughCalculator");
node->add_input_stream("input_ints");
node->add_output_stream("output_ints");
const int kTestValue = 10;
_graph = [[MPPGraph alloc] initWithGraphConfig:config];
[_graph addFrameOutputStream:"output_ints" outputPacketType:MediaPipePacketRaw];
_graph.delegate = self;
WEAKIFY(self);
XCTestExpectation* outputReceived = [self expectationWithDescription:@"output received"];
_packetOutputBlock = ^(MPPGraph* outputGraph, const mediapipe::Packet& packet,
const std::string& outputStreamName) {
STRONGIFY(self);
XCTAssertEqualObjects(outputGraph, _graph);
XCTAssertEqual(outputStreamName, "output_ints");
XCTAssertEqual(packet.Get<int>(), kTestValue);
[outputReceived fulfill];
};
XCTAssert([_graph startWithError:nil]);
XCTAssert([_graph sendPacket:mediapipe::MakePacket<int>(kTestValue).At(mediapipe::Timestamp(1))
intoStream:"input_ints"
error:nil]);
XCTAssert([_graph closeInputStream:"input_ints" error:nil]);
XCTestExpectation* graphDone = [self expectationWithDescription:@"graph done"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
XCTAssert([_graph waitUntilDoneWithError:nil]);
[graphDone fulfill];
});
[self waitForExpectationsWithTimeout:3.0 handler:NULL];
}
@end
+70
View File
@@ -0,0 +1,70 @@
// Copyright 2019 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 <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>
@class MPPInputSource;
/// A delegate that can receive frames from a source.
@protocol MPPInputSourceDelegate <NSObject>
/// Provides the delegate with a new video frame.
@optional
- (void)processVideoFrame:(CVPixelBufferRef)imageBuffer
fromSource:(MPPInputSource*)source __deprecated;
/// Provides the delegate with a new video frame.
@optional
- (void)processVideoFrame:(CVPixelBufferRef)imageBuffer
timestamp:(CMTime)timestamp
fromSource:(MPPInputSource*)source;
// Provides the delegate with a new depth frame data
@optional
- (void)processDepthData:(AVDepthData*)depthData
timestamp:(CMTime)timestamp
fromSource:(MPPInputSource*)source;
@optional
- (void)videoDidPlayToEnd:(CMTime)timestamp;
@end
/// Abstract class for a video source.
@interface MPPInputSource : NSObject
/// The delegate that receives the frames.
@property(weak, nonatomic, readonly) id<MPPInputSourceDelegate> delegate;
/// The dispatch queue on which to schedule the delegate callback.
@property(nonatomic, readonly) dispatch_queue_t delegateQueue;
/// Whether the source is currently running.
@property(nonatomic, getter=isRunning, readonly) BOOL running;
/// Sets the delegate and the queue on which its callback should be invoked.
- (void)setDelegate:(id<MPPInputSourceDelegate>)delegate queue:(dispatch_queue_t)queue;
/// CoreVideo pixel format for the video frames. Defaults to
/// kCVPixelFormatType_32BGRA.
@property(nonatomic) OSType pixelFormatType;
/// Starts the source.
- (void)start;
/// Stops the source.
- (void)stop;
@end
+39
View File
@@ -0,0 +1,39 @@
// Copyright 2019 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 "MPPInputSource.h"
#define ABSTRACT_METHOD \
@throw [NSException exceptionWithName:NSInternalInconsistencyException \
reason:[NSString stringWithFormat:@"You must override %@ in a subclass", \
NSStringFromSelector(_cmd)] \
userInfo:nil];
@implementation MPPInputSource
- (void)setDelegate:(id<MPPInputSourceDelegate>)delegate
queue:(dispatch_queue_t)queue {
_delegate = delegate;
_delegateQueue = queue;
}
- (void)start {
ABSTRACT_METHOD
}
- (void)stop {
ABSTRACT_METHOD
}
@end
+39
View File
@@ -0,0 +1,39 @@
// Copyright 2019 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 <GLKit/GLKit.h>
#import "mediapipe/objc/MPPGLViewRenderer.h"
/// Renders frames in a Core Animation layer.
@interface MPPLayerRenderer : NSObject
@property(nonatomic, readonly) CAEAGLLayer *layer;
/// Updates the layer with a new pixel buffer.
- (void)renderPixelBuffer:(CVPixelBufferRef)pixelBuffer;
/// Sets which way to rotate input frames before rendering them.
/// Default value is MediaPipeFrameRotationNone.
@property(nonatomic) MediaPipeFrameRotationMode frameRotationMode;
/// Sets how to scale the frame within the layer.
/// Default value is MediaPipeFrameScaleScaleToFit.
@property(nonatomic) MediaPipeFrameScaleMode frameScaleMode;
/// If YES, swap left and right. Useful for the front camera.
@property(nonatomic) BOOL mirrored;
@end
+100
View File
@@ -0,0 +1,100 @@
// Copyright 2019 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 "MPPLayerRenderer.h"
@implementation MPPLayerRenderer {
/// Used for rendering to a CAEAGLLayer.
MPPGLViewRenderer *_glRenderer;
/// Used for rendering.
GLuint framebuffer_;
GLuint renderbuffer_;
}
- (instancetype)init {
self = [super init];
if (self) {
_layer = [[CAEAGLLayer alloc] init];
// The default drawable properties are ok.
_layer.opaque = YES;
// Synchronizes presentation with CoreAnimation transactions.
// Avoids desync between GL and CA updates.
// TODO: should this be on by default? It's not on in GLKView,
// but if we add support for AVSampleBufferDisplayLayer we may want to
// make sure the behavior is similar.
if ([_layer respondsToSelector:@selector(setPresentsWithTransaction:)]) {
_layer.presentsWithTransaction = YES;
}
_layer.contentsScale = [[UIScreen mainScreen] scale];
_glRenderer = [[MPPGLViewRenderer alloc] init];
}
return self;
}
- (void)dealloc {
}
- (void)setupFrameBuffer {
[EAGLContext setCurrentContext:_glRenderer.glContext];
glDisable(GL_DEPTH_TEST);
glGenFramebuffers(1, &framebuffer_);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_);
glGenRenderbuffers(1, &renderbuffer_);
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer_);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer_);
[_glRenderer.glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:_layer];
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
NSAssert(status == GL_FRAMEBUFFER_COMPLETE,
@"failed to make complete framebuffer object %x", status);
}
- (void)renderPixelBuffer:(CVPixelBufferRef)pixelBuffer {
if (!framebuffer_) {
[self setupFrameBuffer];
}
GLfloat drawWidth = _layer.bounds.size.width * _layer.contentsScale;
GLfloat drawHeight = _layer.bounds.size.height * _layer.contentsScale;
[EAGLContext setCurrentContext:_glRenderer.glContext];
glViewport(0, 0, drawWidth, drawHeight);
[_glRenderer drawPixelBuffer:pixelBuffer width:drawWidth height:drawHeight];
BOOL success = [_glRenderer.glContext presentRenderbuffer:GL_RENDERBUFFER];
if (!success) NSLog(@"presentRenderbuffer failed");
}
- (MediaPipeFrameRotationMode)frameRotationMode {
return _glRenderer.frameRotationMode;
}
- (void)setFrameRotationMode:(MediaPipeFrameRotationMode)frameRotationMode {
_glRenderer.frameRotationMode = frameRotationMode;
}
- (MediaPipeFrameScaleMode)frameScaleMode {
return _glRenderer.frameScaleMode;
}
- (void)setFrameScaleMode:(MediaPipeFrameScaleMode)frameScaleMode {
_glRenderer.frameScaleMode = frameScaleMode;
}
- (BOOL)mirrored {
return _glRenderer.mirrored;
}
- (void)setMirrored:(BOOL)mirrored {
_glRenderer.mirrored = mirrored;
}
@end
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2019 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 "MPPInputSource.h"
/// A source that outputs frames from a video, played in real time.
/// Not meant for batch processing of video.
@interface MPPPlayerInputSource : MPPInputSource
/// Designated initializer.
- (instancetype)initWithAVAsset:(AVAsset*)video;
/// Skip into video @c time from beginning (time 0), within error of +/- tolerance to closest time.
- (void)seekToTime:(CMTime)time tolerance:(CMTime)tolerance;
/// Set time into video at which to end playback.
- (void)setPlaybackEndTime:(CMTime)time;
/// Returns the current video's timestamp.
- (CMTime)currentPlayerTime;
@end
+127
View File
@@ -0,0 +1,127 @@
// Copyright 2019 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 "MPPPlayerInputSource.h"
#import "mediapipe/objc/MPPDisplayLinkWeakTarget.h"
@implementation MPPPlayerInputSource {
AVAsset* _video;
AVPlayerItem* _videoItem;
AVPlayer* _videoPlayer;
AVPlayerItemVideoOutput* _videoOutput;
CADisplayLink* _videoDisplayLink;
MPPDisplayLinkWeakTarget* _displayLinkWeakTarget;
id _videoEndObserver;
}
- (instancetype)initWithAVAsset:(AVAsset*)video {
self = [super init];
if (self) {
_video = video;
_videoItem = [AVPlayerItem playerItemWithAsset:_video];
// Necessary to ensure the video's preferred transform is respected.
_videoItem.videoComposition = [AVVideoComposition videoCompositionWithPropertiesOfAsset:_video];
_videoOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:@{
(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
(id)kCVPixelBufferIOSurfacePropertiesKey : [NSDictionary dictionary]
}];
_videoOutput.suppressesPlayerRendering = YES;
[_videoItem addOutput:_videoOutput];
_displayLinkWeakTarget =
[[MPPDisplayLinkWeakTarget alloc] initWithTarget:self selector:@selector(videoUpdate:)];
_videoDisplayLink = [CADisplayLink displayLinkWithTarget:_displayLinkWeakTarget
selector:@selector(displayLinkCallback:)];
_videoDisplayLink.paused = YES;
[_videoDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
_videoPlayer = [AVPlayer playerWithPlayerItem:_videoItem];
_videoPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone;
NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
_videoEndObserver = [center addObserverForName:AVPlayerItemDidPlayToEndTimeNotification
object:_videoItem
queue:nil
usingBlock:^(NSNotification* note) {
[self playerItemDidPlayToEnd:note];
}];
}
return self;
}
- (void)start {
[_videoPlayer play];
_videoDisplayLink.paused = NO;
}
- (void)stop {
_videoDisplayLink.paused = YES;
[_videoPlayer pause];
}
- (BOOL)isRunning {
return _videoPlayer.rate != 0.0;
}
- (void)videoUpdate:(CADisplayLink*)sender {
CMTime timestamp = [_videoItem currentTime];
if ([_videoOutput hasNewPixelBufferForItemTime:timestamp]) {
CVPixelBufferRef pixelBuffer =
[_videoOutput copyPixelBufferForItemTime:timestamp itemTimeForDisplay:nil];
if (pixelBuffer)
dispatch_async(self.delegateQueue, ^{
if ([self.delegate respondsToSelector:@selector(processVideoFrame:timestamp:fromSource:)]) {
[self.delegate processVideoFrame:pixelBuffer timestamp:timestamp fromSource:self];
} else if ([self.delegate respondsToSelector:@selector(processVideoFrame:fromSource:)]) {
[self.delegate processVideoFrame:pixelBuffer fromSource:self];
}
CFRelease(pixelBuffer);
});
}
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[_videoDisplayLink invalidate];
_videoPlayer = nil;
}
#pragma mark - NSNotificationCenter / observer
- (void)playerItemDidPlayToEnd:(NSNotification*)notification {
CMTime timestamp = [_videoItem currentTime];
dispatch_async(self.delegateQueue, ^{
if ([self.delegate respondsToSelector:@selector(videoDidPlayToEnd:)]) {
[self.delegate videoDidPlayToEnd:timestamp];
} else {
// Default to loop if no delegate handler set.
[_videoPlayer seekToTime:kCMTimeZero];
}
});
}
- (void)seekToTime:(CMTime)time tolerance:(CMTime)tolerance {
[_videoPlayer seekToTime:time toleranceBefore:tolerance toleranceAfter:tolerance];
}
- (void)setPlaybackEndTime:(CMTime)time {
_videoPlayer.currentItem.forwardPlaybackEndTime = time;
}
- (CMTime)currentPlayerTime {
return _videoPlayer.currentTime;
}
@end
+43
View File
@@ -0,0 +1,43 @@
// Copyright 2019 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 <CoreMedia/CoreMedia.h>
#import <Foundation/Foundation.h>
#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/objc/util.h"
/// Helps convert a CMTime to a MediaPipe timestamp.
@interface MPPTimestampConverter : NSObject
/// The last timestamp returned by timestampForMediaTime:.
@property(nonatomic, readonly) mediapipe::Timestamp lastTimestamp;
/// Initializer.
- (instancetype)init NS_DESIGNATED_INITIALIZER;
/// Resets the object. After this method is called, we can return timestamps
/// that are lower than previously returned timestamps.
- (void)reset;
/// Converts a CMTime to a MediaPipe timestamp. This ensures that MediaPipe
/// timestamps
/// are always increasing: if the provided CMTime has gone backwards (e.g. if
/// it's from a
/// looping video), we shift all timestamps from that point on to keep the
/// output increasing.
/// This state is erased when reset is called.
- (mediapipe::Timestamp)timestampForMediaTime:(CMTime)mediaTime;
@end
+50
View File
@@ -0,0 +1,50 @@
// Copyright 2019 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 "MPPTimestampConverter.h"
@implementation MPPTimestampConverter {
mediapipe::Timestamp _mediapipeTimestamp;
mediapipe::Timestamp _lastTimestamp;
mediapipe::TimestampDiff _timestampOffset;
}
- (instancetype)init
{
self = [super init];
if (self) {
[self reset];
}
return self;
}
- (void)reset {
_mediapipeTimestamp = mediapipe::Timestamp::Min();
_lastTimestamp = _mediapipeTimestamp;
_timestampOffset = 0;
}
- (mediapipe::Timestamp)timestampForMediaTime:(CMTime)mediaTime {
float sampleSeconds = CMTIME_IS_VALID(mediaTime) ? CMTimeGetSeconds(mediaTime) : 0;
const int64 sampleUsec = sampleSeconds * mediapipe::Timestamp::kTimestampUnitsPerSecond;
_mediapipeTimestamp = mediapipe::Timestamp(sampleUsec) + _timestampOffset;
if (_mediapipeTimestamp <= _lastTimestamp) {
_timestampOffset = _timestampOffset + _lastTimestamp + 1 - _mediapipeTimestamp;
_mediapipeTimestamp = _lastTimestamp + 1;
}
_lastTimestamp = _mediapipeTimestamp;
return _mediapipeTimestamp;
}
@end
+48
View File
@@ -0,0 +1,48 @@
// Copyright 2019 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"
#include "mediapipe/framework/port/status.h"
/// Error domain for ::mediapipe::Status errors.
extern NSString *const kGUSGoogleUtilStatusErrorDomain;
/// Key for the ::mediapipe::Status wrapper in an NSError's user info dictionary.
extern NSString *const kGUSGoogleUtilStatusErrorKey;
/// This just wraps ::mediapipe::Status into an Objective-C object.
@interface GUSUtilStatusWrapper : NSObject
@property(nonatomic)::mediapipe::Status status;
+ (instancetype)wrapStatus:(const ::mediapipe::Status &)status;
@end
/// This category adds methods for generating NSError objects from ::mediapipe::Status
/// objects, and vice versa.
@interface NSError (GUSGoogleUtilStatus)
/// Generates an NSError representing a ::mediapipe::Status. Note that NSError always
/// represents an error, so this should not be called with ::mediapipe::Status::OK.
+ (NSError *)gus_errorWithStatus:(const ::mediapipe::Status &)status;
/// Returns a ::mediapipe::Status object representing an NSError. If the NSError was
/// generated from a ::mediapipe::Status, the ::mediapipe::Status returned is identical to
/// the original. Otherwise, this returns a status with code ::util::error::UNKNOWN
/// and a message extracted from the NSError.
@property(nonatomic, readonly)::mediapipe::Status gus_status; // NOLINT(identifier-naming)
@end
+69
View File
@@ -0,0 +1,69 @@
// Copyright 2019 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/objc/NSError+util_status.h"
@implementation GUSUtilStatusWrapper
+ (instancetype)wrapStatus:(const ::mediapipe::Status &)status {
return [[self alloc] initWithStatus:status];
}
- (instancetype)initWithStatus:(const ::mediapipe::Status &)status {
self = [super init];
if (self) {
_status = status;
}
return self;
}
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p; status = %s>",
[self class], self, _status.error_message().c_str()];
}
@end
@implementation NSError (GUSGoogleUtilStatus)
NSString *const kGUSGoogleUtilStatusErrorDomain = @"GoogleUtilStatusErrorDomain";
NSString *const kGUSGoogleUtilStatusErrorKey = @"GUSGoogleUtilStatusErrorKey";
+ (NSError *)gus_errorWithStatus:(const ::mediapipe::Status &)status {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey : @(status.error_message().c_str()),
kGUSGoogleUtilStatusErrorKey : [GUSUtilStatusWrapper wrapStatus:status],
};
NSError *error = [NSError errorWithDomain:kGUSGoogleUtilStatusErrorDomain
code:static_cast<NSInteger>(status.code())
userInfo:userInfo];
return error;
}
- (::mediapipe::Status)gus_status {
NSString *domain = self.domain;
if ([domain isEqual:kGUSGoogleUtilStatusErrorDomain]) {
GUSUtilStatusWrapper *wrapper = self.userInfo[kGUSGoogleUtilStatusErrorKey];
if (wrapper) return wrapper.status;
#if 0
// Unfortunately, util/task/posixerrorspace.h is not in portable status yet.
// TODO: fix that.
} else if ([domain isEqual:NSPOSIXErrorDomain]) {
return ::util::PosixErrorToStatus(self.code, self.localizedDescription.UTF8String);
#endif
}
return ::mediapipe::Status(mediapipe::StatusCode::kUnknown, self.localizedDescription.UTF8String);
}
@end
+54
View File
@@ -0,0 +1,54 @@
// Copyright 2019 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.
//
// OBJC_LINTER
// TODO: check license. This was forked from an internal header.
/**
* __WEAKNAME_ is a private macro used to generate a local variable name related
* to the argument variable name. This generated local variable name is
* intentionally stable across multiple invocations.
*/
#define __WEAKNAME_(variable) variable##_weak_
/**
* WEAKIFY defines a new local variable that is a weak reference to the argument
* variable.
*
* This macro is generally used to capture a weak reference to be captured by an
* Objective-C block to avoid unintentionally extending an object's lifetime or
* avoid causing a retain cycle.
*
* The new local variable's name will be based on the name of the target
* variable and is stable across multiple invocations of WEAKIFY. In general,
* you should not need to invoke WEAKIFY multiple times on the same variable.
*/
#define WEAKIFY(variable) \
__weak __typeof__(variable) __WEAKNAME_(variable) = (variable)
/**
* STRONGIFY defines a new shadow local variable with the same name as the
* argument variable and initialize it with a resolved weak reference based on a
* weak reference created previously using the WEAKIFY macro.
*
* @note:
* This macro must be called within each block scope to prevent nested blocks
* from capturing a strong reference from an outer block.
*/
#define STRONGIFY(variable) \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
__strong __typeof__(variable) variable = \
__WEAKNAME_(variable) _Pragma("clang diagnostic pop")
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

+12
View File
@@ -0,0 +1,12 @@
input_stream: "input_video"
node: {
calculator: "GlLuminanceCalculator"
input_stream: "input_video"
output_stream: "luma_video"
}
node: {
calculator: "GlSobelCalculator"
input_stream: "luma_video"
output_stream: "output_video"
}
+573
View File
@@ -0,0 +1,573 @@
// Copyright 2019 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.
#include "mediapipe/objc/util.h"
#include "absl/base/macros.h"
#include "absl/memory/memory.h"
#include "mediapipe/framework/port/logging.h"
#include "mediapipe/framework/port/ret_check.h"
#include "mediapipe/framework/port/source_location.h"
#include "mediapipe/framework/port/status.h"
#include "mediapipe/framework/port/status_builder.h"
namespace {
// NOTE: you must release the colorspace returned by this function, unless
// it's null.
// Returns an invalid format (all fields 0) if the requested format is
// unsupported.
vImage_CGImageFormat vImageFormatForCVPixelFormat(OSType pixel_format) {
switch (pixel_format) {
case kCVPixelFormatType_OneComponent8:
return {
.bitsPerComponent = 8,
.bitsPerPixel = 8,
.colorSpace = CGColorSpaceCreateDeviceGray(),
.bitmapInfo = kCGImageAlphaNone | kCGBitmapByteOrderDefault,
};
case kCVPixelFormatType_32BGRA:
return {
.bitsPerComponent = 8,
.bitsPerPixel = 32,
.colorSpace = NULL,
.bitmapInfo = kCGImageAlphaFirst | kCGBitmapByteOrder32Little,
};
case kCVPixelFormatType_32RGBA:
return {
.bitsPerComponent = 8,
.bitsPerPixel = 32,
.colorSpace = NULL,
.bitmapInfo = kCGImageAlphaLast | kCGBitmapByteOrderDefault,
};
default:
return {};
}
}
CGColorSpaceRef CreateConversionCGColorSpaceForPixelFormat(
OSType pixel_format) {
// According to vImage documentation, YUV formats require the RGB colorspace
// in which the RGB conversion should be interpreted. sRGB is suggested.
// We cannot just pass sRGB all the time, though, since it breaks with
// monochrome.
switch (pixel_format) {
case kCVPixelFormatType_422YpCbCr8:
case kCVPixelFormatType_4444YpCbCrA8:
case kCVPixelFormatType_4444YpCbCrA8R:
case kCVPixelFormatType_4444AYpCbCr8:
case kCVPixelFormatType_4444AYpCbCr16:
case kCVPixelFormatType_444YpCbCr8:
case kCVPixelFormatType_422YpCbCr16:
case kCVPixelFormatType_422YpCbCr10:
case kCVPixelFormatType_444YpCbCr10:
case kCVPixelFormatType_420YpCbCr8Planar:
case kCVPixelFormatType_420YpCbCr8PlanarFullRange:
case kCVPixelFormatType_422YpCbCr_4A_8BiPlanar:
case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
case kCVPixelFormatType_422YpCbCr8_yuvs:
case kCVPixelFormatType_422YpCbCr8FullRange:
return CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
default:
return NULL;
}
}
vImageConverterRef vImageConverterForCVPixelFormats(OSType src_pixel_format,
OSType dst_pixel_format,
vImage_Error* error) {
static CGFloat default_background[3] = {1.0, 1.0, 1.0};
vImageConverterRef converter = NULL;
vImage_CGImageFormat src_cg_format =
vImageFormatForCVPixelFormat(src_pixel_format);
vImage_CGImageFormat dst_cg_format =
vImageFormatForCVPixelFormat(dst_pixel_format);
// Use CV format functions if available (introduced in iOS 8).
// Weak-linked symbols are NULL when not available.
if (&vImageConverter_CreateForCGToCVImageFormat != NULL) {
// Strangely, there is no function to convert between two
// vImageCVImageFormat, so one side has to use a vImage_CGImageFormat
// that we have to find ourselves.
if (src_cg_format.bitsPerComponent > 0) {
// We can handle source using a CGImageFormat.
// TODO: check the final alpha hint parameter
CGColorSpaceRef cv_color_space =
CreateConversionCGColorSpaceForPixelFormat(dst_pixel_format);
vImageCVImageFormatRef dst_cv_format = vImageCVImageFormat_Create(
dst_pixel_format, kvImage_ARGBToYpCbCrMatrix_ITU_R_709_2,
kCVImageBufferChromaLocation_Center, cv_color_space, 1);
CGColorSpaceRelease(cv_color_space);
converter = vImageConverter_CreateForCGToCVImageFormat(
&src_cg_format, dst_cv_format, default_background,
kvImagePrintDiagnosticsToConsole, error);
vImageCVImageFormat_Release(dst_cv_format);
} else if (dst_cg_format.bitsPerComponent > 0) {
// We can use a CGImageFormat for the destination.
CGColorSpaceRef cv_color_space =
CreateConversionCGColorSpaceForPixelFormat(src_pixel_format);
vImageCVImageFormatRef src_cv_format = vImageCVImageFormat_Create(
src_pixel_format, kvImage_ARGBToYpCbCrMatrix_ITU_R_709_2,
kCVImageBufferChromaLocation_Center, cv_color_space, 1);
CGColorSpaceRelease(cv_color_space);
converter = vImageConverter_CreateForCVToCGImageFormat(
src_cv_format, &dst_cg_format, default_background,
kvImagePrintDiagnosticsToConsole, error);
vImageCVImageFormat_Release(src_cv_format);
}
}
if (!converter) {
// Try a CG to CG conversion.
if (src_cg_format.bitsPerComponent > 0 &&
dst_cg_format.bitsPerComponent > 0) {
converter = vImageConverter_CreateWithCGImageFormat(
&src_cg_format, &dst_cg_format, default_background, kvImageNoFlags,
error);
}
}
CGColorSpaceRelease(src_cg_format.colorSpace);
CGColorSpaceRelease(dst_cg_format.colorSpace);
return converter;
}
} // unnamed namespace
vImage_Error vImageGrayToBGRA(const vImage_Buffer* src, vImage_Buffer* dst) {
static vImageConverterRef converter = NULL;
if (!converter) {
converter = vImageConverterForCVPixelFormats(
kCVPixelFormatType_OneComponent8, kCVPixelFormatType_32BGRA, NULL);
}
return vImageConvert_AnyToAny(converter, src, dst, NULL, kvImageNoFlags);
}
vImage_Error vImageBGRAToGray(const vImage_Buffer* src, vImage_Buffer* dst) {
static vImageConverterRef converter = NULL;
if (!converter) {
converter = vImageConverterForCVPixelFormats(
kCVPixelFormatType_32BGRA, kCVPixelFormatType_OneComponent8, NULL);
}
return vImageConvert_AnyToAny(converter, src, dst, NULL, kvImageNoFlags);
}
vImage_Error vImageRGBAToGray(const vImage_Buffer* src, vImage_Buffer* dst) {
static vImageConverterRef converter = NULL;
if (!converter) {
converter = vImageConverterForCVPixelFormats(
kCVPixelFormatType_32RGBA, kCVPixelFormatType_OneComponent8, NULL);
}
return vImageConvert_AnyToAny(converter, src, dst, NULL, kvImageNoFlags);
}
vImage_Error vImageConvertCVPixelBuffers(CVPixelBufferRef src,
CVPixelBufferRef dst) {
// CGColorSpaceRef srgb_color_space =
// CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
vImage_Error error;
vImageConverterRef converter = vImageConverterForCVPixelFormats(
CVPixelBufferGetPixelFormatType(src),
CVPixelBufferGetPixelFormatType(dst), &error);
if (!converter) {
return error;
}
int src_buffer_count = vImageConverter_GetNumberOfSourceBuffers(converter);
int dst_buffer_count =
vImageConverter_GetNumberOfDestinationBuffers(converter);
vImage_Buffer buffers[8];
if (src_buffer_count + dst_buffer_count > ABSL_ARRAYSIZE(buffers)) {
vImageConverter_Release(converter);
return kvImageMemoryAllocationError;
}
vImage_Buffer* src_bufs = buffers;
vImage_Buffer* dst_bufs = buffers + src_buffer_count;
// vImageBuffer_InitForCopyToCVPixelBuffer can be used only if the converter
// was created by vImageConverter_CreateForCGToCVImageFormat.
// vImageBuffer_InitForCopyFromCVPixelBuffer can be used only if the converter
// was created by vImageConverter_CreateForCVToCGImageFormat.
// There does not seem to be a way to ask the converter for its type; however,
// it is documented that all multi-planar formats are CV formats, so we use
// these calls when there are multiple buffers.
if (src_buffer_count > 1) {
error = vImageBuffer_InitForCopyFromCVPixelBuffer(
src_bufs, converter, src,
kvImageNoAllocate | kvImagePrintDiagnosticsToConsole);
if (error != kvImageNoError) {
vImageConverter_Release(converter);
return error;
}
} else {
*src_bufs = vImageForCVPixelBuffer(src);
}
if (dst_buffer_count > 1) {
error = vImageBuffer_InitForCopyToCVPixelBuffer(
dst_bufs, converter, dst,
kvImageNoAllocate | kvImagePrintDiagnosticsToConsole);
if (error != kvImageNoError) {
vImageConverter_Release(converter);
return error;
}
} else {
*dst_bufs = vImageForCVPixelBuffer(dst);
}
error = vImageConvert_AnyToAny(converter, src_bufs, dst_bufs, NULL,
kvImageNoFlags);
vImageConverter_Release(converter);
return error;
}
void ReleaseMediaPipePacket(void* refcon, const void* base_address) {
auto packet = (mediapipe::Packet*)refcon;
delete packet;
}
CVPixelBufferRef CreateCVPixelBufferForImageFramePacket(
const mediapipe::Packet& image_frame_packet) {
CFHolder<CVPixelBufferRef> buffer;
::mediapipe::Status status =
CreateCVPixelBufferForImageFramePacket(image_frame_packet, &buffer);
MEDIAPIPE_CHECK_OK(status) << "Failed to create CVPixelBufferRef";
return (CVPixelBufferRef)CFRetain(*buffer);
}
::mediapipe::Status CreateCVPixelBufferForImageFramePacket(
const mediapipe::Packet& image_frame_packet,
CFHolder<CVPixelBufferRef>* out_buffer) {
return CreateCVPixelBufferForImageFramePacket(image_frame_packet, false,
out_buffer);
}
::mediapipe::Status CreateCVPixelBufferForImageFramePacket(
const mediapipe::Packet& image_frame_packet, bool can_overwrite,
CFHolder<CVPixelBufferRef>* out_buffer) {
if (!out_buffer) {
return ::mediapipe::InvalidArgumentErrorBuilder(MEDIAPIPE_LOC)
<< "out_buffer cannot be NULL";
}
CFHolder<CVPixelBufferRef> pixel_buffer;
auto packet_copy = absl::make_unique<mediapipe::Packet>(image_frame_packet);
const auto& frame = packet_copy->Get<mediapipe::ImageFrame>();
void* frame_data =
const_cast<void*>(reinterpret_cast<const void*>(frame.PixelData()));
mediapipe::ImageFormat::Format image_format = frame.Format();
OSType pixel_format = 0;
CVReturn status;
switch (image_format) {
case mediapipe::ImageFormat::SRGBA: {
pixel_format = kCVPixelFormatType_32BGRA;
// Swap R and B channels.
vImage_Buffer v_image = vImageForImageFrame(frame);
vImage_Buffer v_dest;
if (can_overwrite) {
v_dest = v_image;
} else {
CVPixelBufferRef pixel_buffer_temp;
status = CVPixelBufferCreate(
kCFAllocatorDefault, frame.Width(), frame.Height(), pixel_format,
GetCVPixelBufferAttributesForGlCompatibility(), &pixel_buffer_temp);
RET_CHECK(status == kCVReturnSuccess)
<< "CVPixelBufferCreate failed: " << status;
pixel_buffer.adopt(pixel_buffer_temp);
status = CVPixelBufferLockBaseAddress(*pixel_buffer,
kCVPixelBufferLock_ReadOnly);
RET_CHECK(status == kCVReturnSuccess)
<< "CVPixelBufferLockBaseAddress failed: " << status;
v_dest = vImageForCVPixelBuffer(*pixel_buffer);
}
const uint8_t permute_map[4] = {2, 1, 0, 3};
vImage_Error vError = vImagePermuteChannels_ARGB8888(
&v_image, &v_dest, permute_map, kvImageNoFlags);
RET_CHECK(vError == kvImageNoError)
<< "vImagePermuteChannels failed: " << vError;
} break;
case mediapipe::ImageFormat::GRAY8:
pixel_format = kCVPixelFormatType_OneComponent8;
break;
default:
return ::mediapipe::UnknownErrorBuilder(MEDIAPIPE_LOC)
<< "unsupported ImageFrame format: " << image_format;
}
if (*pixel_buffer) {
status = CVPixelBufferUnlockBaseAddress(*pixel_buffer,
kCVPixelBufferLock_ReadOnly);
RET_CHECK(status == kCVReturnSuccess)
<< "CVPixelBufferUnlockBaseAddress failed: " << status;
} else {
CVPixelBufferRef pixel_buffer_temp;
status = CVPixelBufferCreateWithBytes(
NULL, frame.Width(), frame.Height(), pixel_format, frame_data,
frame.WidthStep(), ReleaseMediaPipePacket, packet_copy.release(),
GetCVPixelBufferAttributesForGlCompatibility(), &pixel_buffer_temp);
RET_CHECK(status == kCVReturnSuccess)
<< "failed to create pixel buffer: " << status;
pixel_buffer.adopt(pixel_buffer_temp);
}
*out_buffer = pixel_buffer;
return ::mediapipe::OkStatus();
}
::mediapipe::Status CreateCGImageFromCVPixelBuffer(
CVPixelBufferRef image_buffer, CFHolder<CGImageRef>* image) {
CVReturn status =
CVPixelBufferLockBaseAddress(image_buffer, kCVPixelBufferLock_ReadOnly);
RET_CHECK(status == kCVReturnSuccess)
<< "CVPixelBufferLockBaseAddress failed: " << status;
void* base_address = CVPixelBufferGetBaseAddress(image_buffer);
size_t bytes_per_row = CVPixelBufferGetBytesPerRow(image_buffer);
size_t width = CVPixelBufferGetWidth(image_buffer);
size_t height = CVPixelBufferGetHeight(image_buffer);
OSType pixel_format = CVPixelBufferGetPixelFormatType(image_buffer);
CGColorSpaceRef color_space = nullptr;
uint32_t bitmap_info = 0;
switch (pixel_format) {
case kCVPixelFormatType_32BGRA:
color_space = CGColorSpaceCreateDeviceRGB();
bitmap_info =
kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst;
break;
case kCVPixelFormatType_OneComponent8:
color_space = CGColorSpaceCreateDeviceGray();
bitmap_info = kCGImageAlphaNone;
break;
default:
LOG(FATAL) << "Unsupported pixelFormat " << pixel_format;
break;
}
CGContextRef src_context = CGBitmapContextCreate(
base_address, width, height, 8, bytes_per_row, color_space, bitmap_info);
CGImageRef quartz_image = CGBitmapContextCreateImage(src_context);
CGContextRelease(src_context);
CGColorSpaceRelease(color_space);
CFHolder<CGImageRef> cg_image_holder = MakeCFHolderAdopting(quartz_image);
status =
CVPixelBufferUnlockBaseAddress(image_buffer, kCVPixelBufferLock_ReadOnly);
RET_CHECK(status == kCVReturnSuccess)
<< "CVPixelBufferUnlockBaseAddress failed: " << status;
*image = cg_image_holder;
return ::mediapipe::OkStatus();
}
::mediapipe::Status CreateCVPixelBufferFromCGImage(
CGImageRef image, CFHolder<CVPixelBufferRef>* out_buffer) {
size_t width = CGImageGetWidth(image);
size_t height = CGImageGetHeight(image);
CFHolder<CVPixelBufferRef> pixel_buffer;
CVPixelBufferRef pixel_buffer_temp;
CVReturn status = CVPixelBufferCreate(
kCFAllocatorDefault, width, height, kCVPixelFormatType_32BGRA,
GetCVPixelBufferAttributesForGlCompatibility(), &pixel_buffer_temp);
RET_CHECK(status == kCVReturnSuccess)
<< "failed to create pixel buffer: " << status;
pixel_buffer.adopt(pixel_buffer_temp);
status = CVPixelBufferLockBaseAddress(*pixel_buffer, 0);
RET_CHECK(status == kCVReturnSuccess)
<< "CVPixelBufferLockBaseAddress failed: " << status;
void* base_address = CVPixelBufferGetBaseAddress(*pixel_buffer);
CGColorSpaceRef color_space = CGColorSpaceCreateDeviceRGB();
size_t bytes_per_row = CVPixelBufferGetBytesPerRow(*pixel_buffer);
CGContextRef context = CGBitmapContextCreate(
base_address, width, height, 8, bytes_per_row, color_space,
kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
CGRect rect = CGRectMake(0, 0, width, height);
CGContextClearRect(context, rect);
CGContextDrawImage(context, rect, image);
CGContextRelease(context);
CGColorSpaceRelease(color_space);
status = CVPixelBufferUnlockBaseAddress(*pixel_buffer, 0);
RET_CHECK(status == kCVReturnSuccess)
<< "CVPixelBufferUnlockBaseAddress failed: " << status;
*out_buffer = pixel_buffer;
return ::mediapipe::OkStatus();
}
std::unique_ptr<mediapipe::ImageFrame> CreateImageFrameForCVPixelBuffer(
CVPixelBufferRef image_buffer) {
return CreateImageFrameForCVPixelBuffer(image_buffer, false, false);
}
std::unique_ptr<mediapipe::ImageFrame> CreateImageFrameForCVPixelBuffer(
CVPixelBufferRef image_buffer, bool can_overwrite, bool bgr_as_rgb) {
CVReturn status =
CVPixelBufferLockBaseAddress(image_buffer, kCVPixelBufferLock_ReadOnly);
CHECK_EQ(status, kCVReturnSuccess)
<< "CVPixelBufferLockBaseAddress failed: " << status;
void* base_address = CVPixelBufferGetBaseAddress(image_buffer);
size_t bytes_per_row = CVPixelBufferGetBytesPerRow(image_buffer);
size_t width = CVPixelBufferGetWidth(image_buffer);
size_t height = CVPixelBufferGetHeight(image_buffer);
std::unique_ptr<mediapipe::ImageFrame> frame;
CVPixelBufferRetain(image_buffer);
OSType pixel_format = CVPixelBufferGetPixelFormatType(image_buffer);
mediapipe::ImageFormat::Format image_format = mediapipe::ImageFormat::UNKNOWN;
switch (pixel_format) {
case kCVPixelFormatType_32BGRA: {
image_format = mediapipe::ImageFormat::SRGBA;
if (!bgr_as_rgb) {
// Swap R and B channels.
vImage_Buffer v_image = vImageForCVPixelBuffer(image_buffer);
vImage_Buffer v_dest;
if (can_overwrite) {
v_dest = v_image;
} else {
frame = absl::make_unique<mediapipe::ImageFrame>(image_format, width,
height);
v_dest = vImageForImageFrame(*frame);
}
const uint8_t permute_map[4] = {2, 1, 0, 3};
vImage_Error vError = vImagePermuteChannels_ARGB8888(
&v_image, &v_dest, permute_map, kvImageNoFlags);
CHECK(vError == kvImageNoError)
<< "vImagePermuteChannels failed: " << vError;
}
} break;
case kCVPixelFormatType_32RGBA:
image_format = mediapipe::ImageFormat::SRGBA;
break;
case kCVPixelFormatType_24RGB:
image_format = mediapipe::ImageFormat::SRGB;
break;
case kCVPixelFormatType_OneComponent8:
image_format = mediapipe::ImageFormat::GRAY8;
break;
default: {
char format_str[5] = {static_cast<char>(pixel_format >> 24 & 0xFF),
static_cast<char>(pixel_format >> 16 & 0xFF),
static_cast<char>(pixel_format >> 8 & 0xFF),
static_cast<char>(pixel_format & 0xFF), 0};
LOG(FATAL) << "unsupported pixel format: " << format_str;
} break;
}
if (frame) {
// We have already created a new frame that does not reference the buffer.
status = CVPixelBufferUnlockBaseAddress(image_buffer,
kCVPixelBufferLock_ReadOnly);
CHECK_EQ(status, kCVReturnSuccess)
<< "CVPixelBufferUnlockBaseAddress failed: " << status;
CVPixelBufferRelease(image_buffer);
} else {
frame = absl::make_unique<mediapipe::ImageFrame>(
image_format, width, height, bytes_per_row,
reinterpret_cast<uint8*>(base_address), [image_buffer](uint8* x) {
CVPixelBufferUnlockBaseAddress(image_buffer,
kCVPixelBufferLock_ReadOnly);
CVPixelBufferRelease(image_buffer);
});
}
return frame;
}
CFDictionaryRef GetCVPixelBufferAttributesForGlCompatibility() {
static CFDictionaryRef attrs = NULL;
if (!attrs) {
CFDictionaryRef empty_dict = CFDictionaryCreate(
kCFAllocatorDefault, NULL, NULL, 0, &kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
// To ensure compatibility with CVOpenGLESTextureCache, these attributes
// should be present.
const void* keys[] = {
kCVPixelBufferIOSurfacePropertiesKey,
#if TARGET_OS_OSX
kCVPixelFormatOpenGLCompatibility,
#else
kCVPixelFormatOpenGLESCompatibility,
#endif // TARGET_OS_OSX
};
const void* values[] = {empty_dict, kCFBooleanTrue};
attrs = CFDictionaryCreate(
kCFAllocatorDefault, keys, values, ABSL_ARRAYSIZE(values),
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFRelease(empty_dict);
}
return attrs;
}
void DumpCVPixelFormats() {
CFArrayRef pf_descs =
CVPixelFormatDescriptionArrayCreateWithAllPixelFormatTypes(
kCFAllocatorDefault);
CFIndex count = CFArrayGetCount(pf_descs);
CFIndex i;
printf("Core Video Supported Pixel Format Types:\n");
for (i = 0; i < count; i++) {
CFNumberRef pf_num = (CFNumberRef)CFArrayGetValueAtIndex(pf_descs, i);
if (!pf_num) continue;
int pf;
CFNumberGetValue(pf_num, kCFNumberSInt32Type, &pf);
if (pf <= 0x28) {
printf("\nCore Video Pixel Format Type: %d\n", pf);
} else {
printf("\nCore Video Pixel Format Type (FourCC): %c%c%c%c\n",
static_cast<char>(pf >> 24), static_cast<char>(pf >> 16),
static_cast<char>(pf >> 8), static_cast<char>(pf));
}
CFDictionaryRef desc = CVPixelFormatDescriptionCreateWithPixelFormatType(
kCFAllocatorDefault, pf);
CFDictionaryApplyFunction(
desc,
[](const void* key, const void* value, void* context) {
CFStringRef s = CFStringCreateWithFormat(
kCFAllocatorDefault, nullptr, CFSTR(" %@: %@"), key, value);
CFShow(s);
CFRelease(s);
},
nullptr);
CFRelease(desc);
}
CFRelease(pf_descs);
}
+122
View File
@@ -0,0 +1,122 @@
// Copyright 2019 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.
#ifndef MEDIAPIPE_OBJC_UTIL_H_
#define MEDIAPIPE_OBJC_UTIL_H_
#import <Accelerate/Accelerate.h>
#import <CoreFoundation/CoreFoundation.h>
#import <CoreVideo/CoreVideo.h>
#include "mediapipe/framework/formats/image_frame.h"
#include "mediapipe/framework/packet.h"
#include "mediapipe/framework/port/status.h"
#include "mediapipe/objc/CFHolder.h"
// TODO: namespace and/or prefix these. Split up the file.
/// Returns a vImage_Buffer describing the data inside the pixel_buffer.
/// NOTE: the pixel buffer's base address must have been locked before this
/// call, and it must stay locked as long as the vImage_Buffer is in use.
inline vImage_Buffer vImageForCVPixelBuffer(CVPixelBufferRef pixel_buffer) {
return {.data = CVPixelBufferGetBaseAddress(pixel_buffer),
.width = CVPixelBufferGetWidth(pixel_buffer),
.height = CVPixelBufferGetHeight(pixel_buffer),
.rowBytes = CVPixelBufferGetBytesPerRow(pixel_buffer)};
}
/// Returns a vImage_Buffer describing the data inside the ImageFrame.
inline vImage_Buffer vImageForImageFrame(const mediapipe::ImageFrame& frame) {
return {.data = (void*)frame.PixelData(),
.width = static_cast<vImagePixelCount>(frame.Width()),
.height = static_cast<vImagePixelCount>(frame.Height()),
.rowBytes = static_cast<size_t>(frame.WidthStep())};
}
/// Converts a grayscale image without alpha to BGRA format.
vImage_Error vImageGrayToBGRA(const vImage_Buffer* src, vImage_Buffer* dst);
/// Converts a BGRA image to grayscale without alpha.
vImage_Error vImageBGRAToGray(const vImage_Buffer* src, vImage_Buffer* dst);
/// Converts an RGBA image to grayscale without alpha.
vImage_Error vImageRGBAToGray(const vImage_Buffer* src, vImage_Buffer* dst);
/// Copy from a pixel buffer to another, converting pixel format.
/// Both pixel buffers should be locked before calling this.
vImage_Error vImageConvertCVPixelBuffers(CVPixelBufferRef src,
CVPixelBufferRef dst);
/// When storing a mediapipe::Packet* in a CVPixelBuffer's refcon, this can be
/// used as a CVPixelBufferReleaseBytesCallback. This keeps the packet's data
/// alive while the CVPixelBuffer is in use.
void ReleaseMediaPipePacket(void* refcon, const void* base_address);
/// Returns a CVPixelBuffer that references the data inside the packet. The
/// packet must contain an ImageFrame. The CVPixelBuffer manages a copy of
/// the packet, so that the packet's data is kept alive as long as the
/// CVPixelBuffer is in use.
///
/// For formats which are not supported by both image types, it may be
/// necessary to convert the data. This is done by creating a new buffer.
/// If the optional can_overwrite parameter is true, the old buffer may be
/// modified instead.
::mediapipe::Status CreateCVPixelBufferForImageFramePacket(
const mediapipe::Packet& image_frame_packet,
CFHolder<CVPixelBufferRef>* out_buffer);
::mediapipe::Status CreateCVPixelBufferForImageFramePacket(
const mediapipe::Packet& image_frame_packet, bool can_overwrite,
CFHolder<CVPixelBufferRef>* out_buffer);
/// Creates a CVPixelBuffer with a copy of the contents of the CGImage.
::mediapipe::Status CreateCVPixelBufferFromCGImage(
CGImageRef image, CFHolder<CVPixelBufferRef>* out_buffer);
/// Creates a CGImage with a copy of the contents of the CVPixelBuffer.
::mediapipe::Status CreateCGImageFromCVPixelBuffer(
CVPixelBufferRef image_buffer, CFHolder<CGImageRef>* image);
/// DEPRECATED: use the version that returns ::mediapipe::Status instead.
CVPixelBufferRef CreateCVPixelBufferForImageFramePacket(
const mediapipe::Packet& image_frame_packet);
/// Returns an ImageFrame that references the data inside the pixel_buffer.
/// The ImageFrame retains the pixel_buffer and keeps it locked as long as it
/// is in use.
///
/// For formats which are not supported by both image types, it may be
/// necessary to convert the data. This is done by creating a new buffer.
/// If the optional can_overwrite parameter is true, the old buffer may be
/// modified instead.
///
/// ImageFrame does not have a format for BGRA data, so we normally swap the
/// channels to produce RGBA. But many graphs do not care about the order of
/// the channels; in those cases, setting the optional bgr_as_rgb parameter
/// to true skips the channel swap.
std::unique_ptr<mediapipe::ImageFrame> CreateImageFrameForCVPixelBuffer(
CVPixelBufferRef pixel_buffer);
std::unique_ptr<mediapipe::ImageFrame> CreateImageFrameForCVPixelBuffer(
CVPixelBufferRef pixel_buffer, bool can_overwrite, bool bgr_as_rgb);
/// Returns a CFDictionaryRef that can be passed to CVPixelBufferCreate to
/// ensure that the pixel buffer is compatible with OpenGL ES and with
/// CVOpenGLESTextureCacheCreateTextureFromImage.
/// The returned object is persistent and should not be released.
CFDictionaryRef GetCVPixelBufferAttributesForGlCompatibility();
/// Prints debug information about available CoreVideo pixel formats.
/// This prints to stdout.
void DumpCVPixelFormats();
#endif // MEDIAPIPE_OBJC_UTIL_H_