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