Skip to content

Commit 9399041

Browse files
metascroyfacebook-github-bot
authored andcommitted
Add ObjC/Swift bindings for the ImageProcessor (#20051)
Summary: This diff adds ObjC/Swift bindings for the image processor. Only the pixelbuffer variants get ObjC/Swift bindings. The process/process_yuv variants are only accessible from C++ for now. Example: ``` import ExecuTorch // Configure once let config = ImageProcessorConfig( targetWidth: 224, targetHeight: 224, normalization: .imagenet() ) let processor = ImageProcessor(config: config) // Process a CVPixelBuffer (BGRA/RGBA/NV12/P010) let tensor: Tensor<Float> = try processor.process(pixelBuffer) // Output: [1, 3, 224, 224] Float tensor // For video (reuse tensor to avoid allocations) let output = Tensor<Float>([1, 3, 224, 224], Array(repeating: 0, count: 3*224*224)) try processor.process(pixelBuffer, into: output) ``` Differential Revision: D106898406
1 parent 6a3a3e2 commit 9399041

8 files changed

Lines changed: 583 additions & 0 deletions

File tree

extension/apple/BUCK

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ non_fbcode_target(_kind = fb_apple_library,
1111
autoglob_mode = "EXPORT_UNLESS_INTERNAL",
1212
extension_api_only = True,
1313
frameworks = [
14+
"CoreVideo",
1415
"Foundation",
1516
],
1617
preprocessor_flags = [
@@ -29,11 +30,13 @@ non_fbcode_target(_kind = fb_apple_library,
2930
visibility = EXECUTORCH_CLIENTS,
3031
deps = select({
3132
"ovr_config//os:macos": [
33+
"//xplat/executorch/extension/image:image_processorAppleMac",
3234
"//xplat/executorch/extension/module:moduleAppleMac",
3335
"//xplat/executorch/extension/tensor:tensorAppleMac",
3436
"//xplat/executorch/runtime/platform:platformAppleMac",
3537
],
3638
"DEFAULT": [
39+
"//xplat/executorch/extension/image:image_processorApple",
3740
"//xplat/executorch/extension/module:moduleApple",
3841
"//xplat/executorch/extension/tensor:tensorApple",
3942
"//xplat/executorch/runtime/platform:platformApple",
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import CoreVideo
10+
11+
public extension ImageProcessorConfig {
12+
/// Source pixel count (width * height) sentinels for `gpuMinInputPixels`.
13+
static let alwaysGPU = 0
14+
static let alwaysCPU = Int.max
15+
16+
/// Create an image processor config, specifying only the values that differ
17+
/// from the defaults.
18+
///
19+
/// `gpuMinInputPixels` is the minimum source pixel count at which the GPU
20+
/// path may be used; smaller inputs run on the CPU. Use `.alwaysGPU` (0) or
21+
/// `.alwaysCPU` to force a path.
22+
convenience init(
23+
targetWidth: Int,
24+
targetHeight: Int,
25+
resizeMode: ImageResizeMode = .stretch,
26+
letterboxAnchor: ImageLetterboxAnchor = .center,
27+
padValue: Float = 0,
28+
normalization: ImageNormalization = .zeroToOne(),
29+
gpuMinInputPixels: Int = ImageProcessorConfig.defaultGpuMinInputPixels
30+
) {
31+
self.init(
32+
__targetWidth: targetWidth,
33+
targetHeight: targetHeight,
34+
resizeMode: resizeMode,
35+
letterboxAnchor: letterboxAnchor,
36+
padValue: padValue,
37+
normalization: normalization,
38+
gpuMinInputPixels: gpuMinInputPixels)
39+
}
40+
}
41+
42+
public extension ImageProcessor {
43+
/// Process a CVPixelBuffer into a normalized float tensor.
44+
///
45+
/// Auto-detects pixel format from the buffer. Supported formats: BGRA,
46+
/// RGBA, 8-bit NV12, and 10-bit P010. Output is a `Tensor<Float>` with
47+
/// shape `[1, 3, target_height, target_width]`.
48+
///
49+
/// The buffer is treated as already upright: orientation correction is not
50+
/// applied and cannot be derived from a CVPixelBuffer, so the caller is
51+
/// responsible for supplying an upright buffer.
52+
func process(_ pixelBuffer: CVPixelBuffer) throws -> Tensor<Float> {
53+
let anyTensor = try processPixelBuffer(pixelBuffer)
54+
return Tensor<Float>(anyTensor)
55+
}
56+
57+
/// Process a CVPixelBuffer into a caller-provided tensor, reusing its storage.
58+
///
59+
/// Avoids the per-call allocation of `process(_:)`, which matters for
60+
/// sustained video. `tensor` must be a `Tensor<Float>` with shape
61+
/// `[1, 3, target_height, target_width]`; its storage is overwritten and can
62+
/// be reused across frames. The contents are valid until the next call that
63+
/// writes into the same tensor.
64+
///
65+
/// The buffer is treated as already upright (see `process(_:)`).
66+
func process(_ pixelBuffer: CVPixelBuffer, into tensor: Tensor<Float>) throws {
67+
try processPixelBuffer(pixelBuffer, into: tensor.anyTensor)
68+
}
69+
70+
/// Letterbox padding (per side, in pixels) applied for a source of the given
71+
/// size: `x` is the left/right pad and `y` the top/bottom pad of the resized
72+
/// content. Returns `(0, 0)` for the stretch resize mode or the top-left
73+
/// anchor. Lets callers map the padded output back to the source region.
74+
func computeLetterboxPadding(inputWidth: Int, inputHeight: Int) -> (x: Int, y: Int) {
75+
let padding = __computeLetterboxPadding(forInputWidth: inputWidth, height: inputHeight)
76+
return (Int(padding.x), Int(padding.y))
77+
}
78+
}

extension/apple/ExecuTorch/Exported/ExecuTorch.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#import "ExecuTorchBackendOption.h"
1010
#import "ExecuTorchBackendOptionsMap.h"
1111
#import "ExecuTorchError.h"
12+
#import "ExecuTorchImageProcessor.h"
1213
#import "ExecuTorchLog.h"
1314
#import "ExecuTorchModule.h"
1415
#import "ExecuTorchTensor.h"
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#import <CoreVideo/CoreVideo.h>
10+
#import <Foundation/Foundation.h>
11+
12+
#import "ExecuTorchTensor.h"
13+
14+
NS_ASSUME_NONNULL_BEGIN
15+
16+
typedef NS_ENUM(uint8_t, ExecuTorchImageResizeMode) {
17+
ExecuTorchImageResizeModeStretch,
18+
ExecuTorchImageResizeModeLetterbox,
19+
} NS_SWIFT_NAME(ImageResizeMode);
20+
21+
typedef NS_ENUM(uint8_t, ExecuTorchImageLetterboxAnchor) {
22+
ExecuTorchImageLetterboxAnchorCenter,
23+
ExecuTorchImageLetterboxAnchorTopLeft,
24+
} NS_SWIFT_NAME(ImageLetterboxAnchor);
25+
26+
NS_SWIFT_NAME(ImageNormalization)
27+
__attribute__((objc_subclassing_restricted))
28+
@interface ExecuTorchImageNormalization : NSObject
29+
30+
+ (instancetype)zeroToOne;
31+
+ (instancetype)imagenet;
32+
33+
+ (instancetype)new NS_UNAVAILABLE;
34+
- (instancetype)init NS_UNAVAILABLE;
35+
36+
@end
37+
38+
NS_SWIFT_NAME(ImageProcessorConfig)
39+
__attribute__((objc_subclassing_restricted))
40+
@interface ExecuTorchImageProcessorConfig : NSObject
41+
42+
@property(nonatomic, readonly) NSInteger targetWidth;
43+
@property(nonatomic, readonly) NSInteger targetHeight;
44+
@property(nonatomic, readonly) ExecuTorchImageResizeMode resizeMode;
45+
@property(nonatomic, readonly) ExecuTorchImageLetterboxAnchor letterboxAnchor;
46+
@property(nonatomic, readonly) float padValue;
47+
@property(nonatomic, readonly) ExecuTorchImageNormalization *normalization;
48+
// Minimum source pixel count (width * height) at which the GPU path may be
49+
// used; smaller inputs run on the CPU. 0 forces GPU, NSIntegerMax forces CPU.
50+
@property(nonatomic, readonly) NSInteger gpuMinInputPixels;
51+
52+
// Default value for gpuMinInputPixels (mirrors the C++ config default).
53+
@property(class, nonatomic, readonly) NSInteger defaultGpuMinInputPixels;
54+
55+
- (instancetype)initWithTargetWidth:(NSInteger)targetWidth
56+
targetHeight:(NSInteger)targetHeight
57+
resizeMode:(ExecuTorchImageResizeMode)resizeMode
58+
letterboxAnchor:(ExecuTorchImageLetterboxAnchor)letterboxAnchor
59+
padValue:(float)padValue
60+
normalization:(ExecuTorchImageNormalization *)normalization
61+
gpuMinInputPixels:(NSInteger)gpuMinInputPixels NS_REFINED_FOR_SWIFT;
62+
63+
+ (instancetype)new NS_UNAVAILABLE;
64+
- (instancetype)init NS_UNAVAILABLE;
65+
66+
@end
67+
68+
/// Thread-safety: ExecuTorchImageProcessor is NOT thread-safe per instance.
69+
/// Internal scratch buffers are mutated during processing. Use one instance
70+
/// per concurrent caller. Different instances are safe to use concurrently.
71+
NS_SWIFT_NAME(ImageProcessor)
72+
__attribute__((objc_subclassing_restricted))
73+
@interface ExecuTorchImageProcessor : NSObject
74+
75+
@property(nonatomic, readonly) ExecuTorchImageProcessorConfig *config;
76+
77+
- (instancetype)initWithConfig:(ExecuTorchImageProcessorConfig *)config;
78+
79+
/// Process a CVPixelBuffer into a normalized float tensor.
80+
///
81+
/// Auto-detects pixel format from the buffer's metadata. Supported
82+
/// formats: BGRA, RGBA, 8-bit NV12, and 10-bit P010 (P010 is narrowed to NV12
83+
/// internally). Other formats return an error.
84+
///
85+
/// The buffer is treated as already upright. Orientation correction is not
86+
/// applied and cannot be derived from a CVPixelBuffer, so the caller is
87+
/// responsible for supplying an upright buffer (e.g. by configuring the
88+
/// capture connection's orientation).
89+
///
90+
/// @param pixelBuffer The input pixel buffer.
91+
/// @param error On failure, set to an NSError describing what went wrong.
92+
/// @return An ExecuTorchTensor with shape [1, 3, H, W] (CHW), or nil on failure.
93+
- (nullable ExecuTorchTensor *)processPixelBuffer:(_Nullable CVPixelBufferRef)pixelBuffer
94+
error:(NSError **)error;
95+
96+
/// Process a CVPixelBuffer into a caller-provided tensor, reusing its storage.
97+
///
98+
/// Avoids the per-call output allocation of processPixelBuffer:error:, which
99+
/// matters for sustained video. `tensor` must be a Float tensor shaped
100+
/// [1, 3, targetHeight, targetWidth]; its storage is overwritten and can be
101+
/// reused across frames. The result aliases `tensor`, so the caller must
102+
/// finish using the previous result before the next call.
103+
///
104+
/// @param pixelBuffer The input pixel buffer.
105+
/// @param tensor The output tensor to fill.
106+
/// @param error On failure, set to an NSError describing what went wrong.
107+
/// @return YES on success, NO on failure.
108+
- (BOOL)processPixelBuffer:(_Nullable CVPixelBufferRef)pixelBuffer
109+
intoTensor:(ExecuTorchTensor *)tensor
110+
error:(NSError **)error;
111+
112+
/// Letterbox padding (per side, in pixels) the processor applies for a source
113+
/// of the given size: `x` is the left/right pad and `y` the top/bottom pad of
114+
/// the resized content. Returns {0, 0} for the stretch resize mode or the
115+
/// top-left anchor. Lets callers map the padded output back to the source
116+
/// region without replicating the resize geometry.
117+
///
118+
/// @param inputWidth The source pixel width.
119+
/// @param inputHeight The source pixel height.
120+
/// @return The {x, y} padding (integer values stored in a CGPoint).
121+
- (CGPoint)computeLetterboxPaddingForInputWidth:(NSInteger)inputWidth
122+
height:(NSInteger)inputHeight
123+
NS_REFINED_FOR_SWIFT;
124+
125+
+ (instancetype)new NS_UNAVAILABLE;
126+
- (instancetype)init NS_UNAVAILABLE;
127+
128+
@end
129+
130+
NS_ASSUME_NONNULL_END

0 commit comments

Comments
 (0)