1// 2// EXFaceDetectorPointTransformCalculator.m 3// Exponent 4// 5// Created by Stanisław Chmiela on 30.10.2017. 6// Copyright © 2017 650 Industries. All rights reserved. 7// 8 9#import <EXFaceDetector/EXCSBufferOrientationCalculator.h> 10 11#define TRANSFORM_PI_2 CGAffineTransformMake(0, 1, -1, 0, 0, 0) 12#define TRANSFORM_PI CGAffineTransformMake(-1, 0, 0, -1, 0, 0) 13#define TRANSFORM_NEGATIVE_PI_2 CGAffineTransformMake(0, -1, 1, 0, 0, 0) 14#define TRANSFORM_X_SYMETRY CGAffineTransformMake(1, 0, 0, -1, 0, 0) 15#define TRANSFORM_Y_SYMETRY CGAffineTransformMake(-1, 0, 0, 1, 0, 0) 16 17/* 18 * The purpose of this class is to calculate the transform used to translate 19 * face detected by MLKit using CMSampleBufferRef to proper view coordinates. 20 * 21 * CSSampleBuffer always operates on image in LandscapeLeft orientation. 22 * Therefore, all obtained points needs to be rotated, translated and scaled properly in order to fit original position on preview. 23 * 24 * Let's see the behavior on a specific example. Imagine an app with portrait screen orientation. 25 * 26 * ``` 27 * +---+ 28 * |^^ | // by ^^ we shall denote a happy face, ^^ 29 * | | 30 * | | 31 * +---+ 32 * - // by - we shall denote the bottom of the interface. 33 * ``` 34 * 35 * For buffer orientation, hovewer, is landscape right, which means face will be positioned in buffer as indicated: 36 * 37 * ``` 38 * +---------------+ 39 * | >| 40 * | >| 41 * | | 42 * +---------------+ 43 * ``` 44 * 45 * Which would result in displayng improper face position on preview: 46 * ``` 47 * +---+ 48 * |^^ | // by ^^ we shall denote a happy face, ^^ 49 * |x | // by xx we shall denote face position as detected by MLKit 50 * |x | 51 * +---+ 52 * - // by - we shall denote the bottom of the interface. 53 * ``` 54 * 55 * In order to display face properly we need to rotate it 90 degrees counterclockwise. However, rotation alon would give still improper results: 56 * ``` 57 * +---+ 58 * xx |^^ | // by ^^ we shall denote a happy face, ^^ 59 * | | // by xx we shall denote face position as detected by MLKit 60 * | | 61 * +---+ 62 * - // by - and | we shall denote boundaries of an interface 63 * ``` 64 * Which is the result of rotation being performed aroun (0,0) point. Now we need to add translation by interface width: 65 * ``` 66 * +---+ 67 * |** | // by xx we shall denote face position as detected by MLKit 68 * | | 69 * | | 70 * +---+ 71 * - // by - and | we shall denote boundaries of an interface 72 * ``` 73 * And we have face correctly displayed. 74 * Last think we need to remember is performing x or y symetry for when preview is mirrored (for front camera) in portrait or landscape orientation respectively. 75 */ 76 77@implementation EXCSBufferOrientationCalculator 78 79+ (CGAffineTransform)pointTransformForInterfaceOrientation:(UIInterfaceOrientation)orientation 80 forBufferWidth:(CGFloat)bufferWidth 81 andBufferHeight:(CGFloat)bufferHeight 82 andVideoWidth:(CGFloat)videoWidth 83 andVideoHeight:(CGFloat)videoHeight 84 andMirrored:(BOOL)mirrored 85{ 86 CGAffineTransform rotationTransform = CGAffineTransformConcat( 87 [EXCSBufferOrientationCalculator rotationTransformForOrientation:orientation], 88 [EXCSBufferOrientationCalculator transformForMirror:orientation forMirrored:mirrored]); 89 CGAffineTransform rotationWithTranslation = [EXCSBufferOrientationCalculator rotationWithTranslation:rotationTransform forVideoWidth:videoWidth forVideoHeight:videoHeight]; 90 91 CGFloat scaleFactor = [EXCSBufferOrientationCalculator scaleForCropWithPreservingAspectRatioforBufferWidth:bufferWidth andBufferHeight:bufferHeight andVideoWidth:videoWidth andVideoHeight:videoHeight]; 92 93 CGAffineTransform cropTranslation = [EXCSBufferOrientationCalculator cropTranslationsTransformationForRotation:rotationWithTranslation andScale:scaleFactor andBufferWidth:bufferWidth andBufferHeight:bufferHeight andVideoWidth:videoWidth andVideoHeight:videoHeight]; 94 CGAffineTransform rotationWithAllTranslations = CGAffineTransformConcat(rotationWithTranslation, cropTranslation); 95 return CGAffineTransformScale(rotationWithAllTranslations, scaleFactor, scaleFactor); 96} 97 98+ (CGAffineTransform)rotationTransformForOrientation:(UIInterfaceOrientation)orientation 99{ 100 switch (orientation) { 101 case (UIInterfaceOrientationLandscapeRight): 102 return CGAffineTransformIdentity; 103 case (UIInterfaceOrientationPortrait): 104 return TRANSFORM_PI_2; 105 case (UIInterfaceOrientationLandscapeLeft): 106 return TRANSFORM_PI; 107 case (UIInterfaceOrientationPortraitUpsideDown): 108 return TRANSFORM_NEGATIVE_PI_2; 109 default: 110 return CGAffineTransformIdentity; 111 } 112} 113 114+ (CGAffineTransform)transformForMirror:(UIInterfaceOrientation)orientation forMirrored:(BOOL)mirrored 115{ 116 if(mirrored) 117 { 118 if(UIInterfaceOrientationIsLandscape(orientation)) { 119 return TRANSFORM_X_SYMETRY; 120 } else { 121 return TRANSFORM_Y_SYMETRY; 122 } 123 } else { 124 return CGAffineTransformIdentity; 125 } 126} 127 128+(CGAffineTransform)rotationWithTranslation:(CGAffineTransform)rotationTransformation 129 forVideoWidth:(CGFloat)videoWidth 130 forVideoHeight:(CGFloat)videoHeight 131{ 132 BOOL translateX = rotationTransformation.a == -1 || rotationTransformation.c == -1; 133 BOOL translateY = rotationTransformation.b == -1 || rotationTransformation.d == -1; 134 CGAffineTransform translationTransformation = CGAffineTransformTranslate(CGAffineTransformIdentity, translateX ? videoWidth : 0, translateY ? videoHeight : 0); 135 return CGAffineTransformConcat(rotationTransformation, translationTransformation); 136} 137 138+(CGFloat)scaleForCropWithPreservingAspectRatioforBufferWidth:(CGFloat)bufferWidth 139 andBufferHeight:(CGFloat)bufferHeight 140 andVideoWidth:(CGFloat)videoWidth 141 andVideoHeight:(CGFloat)videoHeight 142{ 143 float widthFactor = videoWidth / bufferWidth; 144 float heightFactor = videoHeight / bufferHeight; 145 146 return MAX(widthFactor, heightFactor); 147} 148 149+(CGAffineTransform)cropTranslationsTransformationForRotation:(CGAffineTransform)rotation 150 andScale:(CGFloat)scale 151 andBufferWidth:(CGFloat)bufferWidth 152 andBufferHeight:(CGFloat)bufferHeight 153 andVideoWidth:(CGFloat)videoWidth 154 andVideoHeight:(CGFloat)videoHeight 155{ 156 float xOffset = -((bufferWidth * scale) - videoWidth) / 2; 157 float yOffset = -((bufferHeight * scale) - videoHeight) / 2; 158 159 // This is actually multipling rotation matrix by crop translations point. 160 float xTranslation = (rotation.a + rotation.c) * xOffset; 161 float yTranslation = (rotation.b + rotation.d) * yOffset; 162 163 return CGAffineTransformMakeTranslation(xTranslation, yTranslation); 164} 165 166@end 167