278 lines
11 KiB
Objective-C
278 lines
11 KiB
Objective-C
//
|
|
// RNFaceDetectorPointTransformCalculator.m
|
|
// RCTCamera
|
|
//
|
|
// Created by Joao Guilherme Daros Fidelis on 21/01/18.
|
|
//
|
|
|
|
#import "RNFaceDetectorPointTransformCalculator.h"
|
|
|
|
#define cDefaultFloatComparisonEpsilon 0.0001
|
|
#define cModEqualFloatsWithEpsilon(dividend, divisor, modulo, epsilon) \
|
|
fabs( fmod(dividend, divisor) - modulo ) < epsilon
|
|
#define cModEqualFloats(dividend, divisor, modulo) \
|
|
cModEqualFloatsWithEpsilon(dividend, divisor, modulo, cDefaultFloatComparisonEpsilon)
|
|
|
|
/*
|
|
* The purpose of this class is to calculate the transform used to translate
|
|
* face detected by Google Mobile Vision to proper view coordinates.
|
|
*
|
|
* When an Expo app locks interface orientatation in `app.json` or with `ScreenOrientation.allow`,
|
|
* interface gets locked, but device orientation still can change. It looks like Google Mobile Vision
|
|
* listens to device orientation changes and transforms coordinates of faces as if the device orientation
|
|
* always equals interface orientation (which in Expo is not the case).
|
|
*
|
|
* Let's see the behavior on a specific example. Imagine an app with screen orientation locked to portrait.
|
|
*
|
|
* ```
|
|
* +---+
|
|
* |^^ | // by ^^ we shall denote a happy face, ^^
|
|
* | |
|
|
* | |
|
|
* +---+
|
|
* - // by - we shall denote the bottom of the interface.
|
|
* ```
|
|
*
|
|
* When the device is being held like this face is properly reported in (0, 0).
|
|
* However, when we rotate the device to landscape, the situation looks like this:
|
|
*
|
|
* ```
|
|
* +---------------+
|
|
* |^^ x| // by xx we shall where the face should by according to GMV detector.
|
|
* || x| // note that interface is still portrait-oriented
|
|
* | |
|
|
* +---------------+
|
|
* ```
|
|
*
|
|
* For GMV, which thinks that the interface is in landscape (`UIDeviceOrientation` changed to landscape)
|
|
* the face is in `(0, 0)`. However, for our app `(0, 0)` is in the top left corner of the device --
|
|
* -- that's where the face indicator gets positioned.
|
|
*
|
|
* That's when we have to rotate and translate the face indicator. Here we have to rotate it by -90 degrees.
|
|
*
|
|
* ```
|
|
* +---------------+
|
|
* |^^ |xx // something is still wrong
|
|
* || |
|
|
* | |
|
|
* +---------------+
|
|
* ```
|
|
*
|
|
* Not only must we rotate the indicator, we also have to translate it. Here by (-videoWidth, 0).
|
|
*
|
|
* ```
|
|
* +---------------+
|
|
* |** | // detected eyes glow inside the face indicator
|
|
* || |
|
|
* | |
|
|
* +---------------+
|
|
* ```
|
|
*
|
|
* Fixing this issue is the purpose of this whole class.
|
|
*
|
|
*/
|
|
|
|
|
|
typedef NS_ENUM(NSInteger, RNTranslationEnum) {
|
|
RNTranslateYNegativeWidth,
|
|
RNTranslateXNegativeHeight,
|
|
RNTranslateXYNegative,
|
|
RNTranslateYXNegative
|
|
};
|
|
|
|
@interface RNFaceDetectorPointTransformCalculator()
|
|
|
|
@property (assign, nonatomic) AVCaptureVideoOrientation fromOrientation;
|
|
@property (assign, nonatomic) AVCaptureVideoOrientation toOrientation;
|
|
@property (assign, nonatomic) CGFloat videoWidth;
|
|
@property (assign, nonatomic) CGFloat videoHeight;
|
|
|
|
@end
|
|
|
|
@implementation RNFaceDetectorPointTransformCalculator
|
|
|
|
- (instancetype)initToTransformFromOrientation:(AVCaptureVideoOrientation)fromOrientation toOrientation:(AVCaptureVideoOrientation)toOrientation forVideoWidth:(CGFloat)videoWidth andVideoHeight:(CGFloat)videoHeight
|
|
{
|
|
self = [super init];
|
|
if (self) {
|
|
_fromOrientation = fromOrientation;
|
|
_toOrientation = toOrientation;
|
|
_videoWidth = videoWidth;
|
|
_videoHeight = videoHeight;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (CGFloat)rotation
|
|
{
|
|
if (_fromOrientation == _toOrientation) {
|
|
return 0;
|
|
}
|
|
|
|
AVCaptureVideoOrientation firstOrientation = MIN(_fromOrientation, _toOrientation);
|
|
AVCaptureVideoOrientation secondOrientation = MAX(_fromOrientation, _toOrientation);
|
|
CGFloat angle = [[[self class] getRotationDictionary][@(firstOrientation)][@(secondOrientation)] doubleValue];
|
|
|
|
/*
|
|
* It turns out that if you need to rotate the indicator by -90 degrees to get it from
|
|
* landscape left (Device orientation) to portrait (Interface Orientation),
|
|
* to get the indicator from portrait (D) to landscape left (I), you need to rotate it by 90 degrees.
|
|
* Same analogy `r(1, 2) == x <==> r(2, 1) == -x` is true for every other transformation.
|
|
*/
|
|
if (_fromOrientation > _toOrientation) {
|
|
angle = -angle;
|
|
}
|
|
|
|
return angle;
|
|
}
|
|
|
|
- (CGPoint)translation
|
|
{
|
|
if (_fromOrientation == _toOrientation) {
|
|
return CGPointZero;
|
|
}
|
|
|
|
AVCaptureVideoOrientation firstOrientation = MIN(_fromOrientation, _toOrientation);
|
|
AVCaptureVideoOrientation secondOrientation = MAX(_fromOrientation, _toOrientation);
|
|
RNTranslationEnum enumValue = [[[self class] getTranslationDictionary][@(firstOrientation)][@(secondOrientation)] intValue];
|
|
|
|
CGPoint translation = [self translationForEnum:enumValue];
|
|
|
|
/*
|
|
* Here the analogy is a little bit more complicated than when calculating rotation.
|
|
* It turns out that if you need to translate the _rotated_ indicator
|
|
* from landscape left (D) to portrait (I) by `(-videoWidth, 0)` (see top class comment),
|
|
* to translate the rotated indicator from portrait (D) to landscape left (D) you need to translate it
|
|
* by `(0, -videoWidth)`.
|
|
*
|
|
* ```
|
|
* +-------+
|
|
* +--------------------+ |^^ | // ^^ == happy face
|
|
* |^^ | | |
|
|
* | | | |
|
|
* | | | || // | or - == bottom of the interface
|
|
* | | | |
|
|
* | | |x | // xx == initial face indicator
|
|
* +--------------------+ |x |
|
|
* - +-------+
|
|
* oo // oo == rotated face indicator
|
|
* ```
|
|
*
|
|
* As we can see, the indicator has to be translated by `(0, -videoWidth)` to match with the happy face.
|
|
*
|
|
* It turns out, that `(0, -videoWidth) == translation(device: 1, interface: 4)` can be calculated by
|
|
* rotating `translation(device: 4, interface: 1) == (-videoWidth, 0)` by `rotation(4, 1) == -90deg`.
|
|
*
|
|
* One might think that the same analogy `t(1, 2) == r(2, 1)[t(2, 1)]` works always,
|
|
* but here this assumption would be wrong. The analogy works only when device and interface rotations
|
|
* differ by 90 or -90 degrees.
|
|
*
|
|
* Otherwise (when transforming from/to portrait/upside or landscape left/right)
|
|
* `translation(1, 2) == translation(2, 1).
|
|
*/
|
|
if (_fromOrientation > _toOrientation) {
|
|
CGFloat translationRotationAngle = [self rotation];
|
|
if (cModEqualFloats(translationRotationAngle + M_PI, M_PI, M_PI_2)) {
|
|
CGAffineTransform transform = CGAffineTransformIdentity;
|
|
transform = CGAffineTransformRotate(transform, translationRotationAngle);
|
|
translation = CGPointApplyAffineTransform(translation, transform);
|
|
}
|
|
}
|
|
|
|
return translation;
|
|
}
|
|
|
|
- (CGAffineTransform)transform
|
|
{
|
|
CGAffineTransform transform = CGAffineTransformIdentity;
|
|
|
|
CGFloat rotation = [self rotation];
|
|
transform = CGAffineTransformRotate(transform, rotation);
|
|
|
|
CGPoint translation = [self translation];
|
|
transform = CGAffineTransformTranslate(transform, translation.x, translation.y);
|
|
|
|
return transform;
|
|
}
|
|
|
|
# pragma mark - Enum conversion
|
|
|
|
- (CGPoint)translationForEnum:(RNTranslationEnum)enumValue
|
|
{
|
|
switch (enumValue) {
|
|
case RNTranslateXNegativeHeight:
|
|
return CGPointMake(-_videoHeight, 0);
|
|
case RNTranslateYNegativeWidth:
|
|
return CGPointMake(0, -_videoWidth);
|
|
case RNTranslateXYNegative:
|
|
return CGPointMake(-_videoWidth, -_videoHeight);
|
|
case RNTranslateYXNegative:
|
|
return CGPointMake(-_videoHeight, -_videoWidth);
|
|
}
|
|
}
|
|
|
|
# pragma mark - Lookup tables
|
|
|
|
static NSDictionary<NSNumber *, NSDictionary<NSNumber *, NSNumber *> *> *rotationDictionary = nil;
|
|
static NSDictionary<NSNumber *, NSDictionary<NSNumber *, NSNumber *> *> *translationDictionary = nil;
|
|
|
|
+ (NSDictionary<NSNumber *, NSDictionary<NSNumber *, NSNumber *> *> *) getRotationDictionary
|
|
{
|
|
if (rotationDictionary == nil) {
|
|
[self initRotationDictionary];
|
|
}
|
|
|
|
return rotationDictionary;
|
|
}
|
|
|
|
+ (NSDictionary<NSNumber *, NSDictionary<NSNumber *, NSNumber *> *> *) getTranslationDictionary
|
|
{
|
|
if (translationDictionary == nil) {
|
|
[self initTranslationDictionary];
|
|
}
|
|
|
|
return translationDictionary;
|
|
}
|
|
|
|
# pragma mark - Initialize dictionaries
|
|
|
|
// If you wonder why this dictionary is half-empty, see comment inside `- (CGFloat)rotation`. It may help you.
|
|
+ (void)initRotationDictionary
|
|
{
|
|
rotationDictionary = @{
|
|
@(AVCaptureVideoOrientationPortrait): @{
|
|
@(AVCaptureVideoOrientationLandscapeLeft) : @(M_PI_2),
|
|
@(AVCaptureVideoOrientationLandscapeRight) : @(-M_PI_2),
|
|
@(AVCaptureVideoOrientationPortraitUpsideDown) : @(M_PI),
|
|
},
|
|
@(AVCaptureVideoOrientationPortraitUpsideDown): @{
|
|
@(AVCaptureVideoOrientationLandscapeLeft) : @(-M_PI_2),
|
|
@(AVCaptureVideoOrientationLandscapeRight) : @(M_PI_2)
|
|
},
|
|
@(AVCaptureVideoOrientationLandscapeRight): @{
|
|
@(AVCaptureVideoOrientationLandscapeLeft) : @(M_PI)
|
|
}
|
|
};
|
|
}
|
|
|
|
// If you wonder why this dictionary is half-empty, see comment inside `- (CGPoint)translation`. It may help you.
|
|
+ (void)initTranslationDictionary
|
|
{
|
|
translationDictionary = @{
|
|
@(AVCaptureVideoOrientationPortrait): @{
|
|
@(AVCaptureVideoOrientationLandscapeLeft) : @(RNTranslateYNegativeWidth),
|
|
@(AVCaptureVideoOrientationLandscapeRight) : @(RNTranslateXNegativeHeight),
|
|
@(AVCaptureVideoOrientationPortraitUpsideDown) : @(RNTranslateYXNegative)
|
|
},
|
|
@(AVCaptureVideoOrientationPortraitUpsideDown): @{
|
|
@(AVCaptureVideoOrientationLandscapeLeft) : @(RNTranslateXNegativeHeight),
|
|
@(AVCaptureVideoOrientationLandscapeRight) : @(RNTranslateYNegativeWidth)
|
|
},
|
|
@(AVCaptureVideoOrientationLandscapeRight): @{
|
|
@(AVCaptureVideoOrientationLandscapeLeft) : @(RNTranslateXYNegative)
|
|
}
|
|
};
|
|
}
|
|
|
|
@end
|