@@ -5,13 +5,17 @@ import { Camera } from 'expo-camera';
5
5
6
6
import * as tf from '@tensorflow/tfjs' ;
7
7
import * as posedetection from '@tensorflow-models/pose-detection' ;
8
+ import * as ScreenOrientation from 'expo-screen-orientation' ;
8
9
import { cameraWithTensors } from '@tensorflow/tfjs-react-native' ;
9
10
import Svg , { Circle } from 'react-native-svg' ;
10
11
import { ExpoWebGLRenderingContext } from 'expo-gl' ;
11
12
12
13
// tslint:disable-next-line: variable-name
13
14
const TensorCamera = cameraWithTensors ( Camera ) ;
14
15
16
+ const IS_ANDROID = Platform . OS === 'android' ;
17
+ const IS_IOS = Platform . OS === 'ios' ;
18
+
15
19
// Camera preview size.
16
20
//
17
21
// From experiments, to render camera feed without distortion, 16:9 ratio
@@ -20,18 +24,18 @@ const TensorCamera = cameraWithTensors(Camera);
20
24
//
21
25
// This might not cover all cases.
22
26
const CAM_PREVIEW_WIDTH = Dimensions . get ( 'window' ) . width ;
23
- const CAM_PREVIEW_HEIGHT =
24
- CAM_PREVIEW_WIDTH / ( Platform . OS === 'ios' ? 9 / 16 : 3 / 4 ) ;
27
+ const CAM_PREVIEW_HEIGHT = CAM_PREVIEW_WIDTH / ( IS_IOS ? 9 / 16 : 3 / 4 ) ;
25
28
26
29
// The score threshold for pose detection results.
27
30
const MIN_KEYPOINT_SCORE = 0.3 ;
28
31
29
32
// The size of the resized output from TensorCamera.
30
33
//
31
34
// For movenet, the size here doesn't matter too much because the model will
32
- // preprocess the input (crop, resize, etc).
33
- const OUTPUT_TENSOR_WIDTH = 240 ;
34
- const OUTPUT_TENSOR_HEIGHT = 320 ;
35
+ // preprocess the input (crop, resize, etc). For best result, use the size that
36
+ // doesn't distort the image.
37
+ const OUTPUT_TENSOR_WIDTH = 180 ;
38
+ const OUTPUT_TENSOR_HEIGHT = OUTPUT_TENSOR_WIDTH / ( IS_IOS ? 9 / 16 : 3 / 4 ) ;
35
39
36
40
// Whether to auto-render TensorCamera preview.
37
41
const AUTO_RENDER = false ;
@@ -42,9 +46,20 @@ export default function App() {
42
46
const [ model , setModel ] = useState < posedetection . PoseDetector > ( ) ;
43
47
const [ poses , setPoses ] = useState < posedetection . Pose [ ] > ( ) ;
44
48
const [ fps , setFps ] = useState ( 0 ) ;
49
+ const [ orientation , setOrientation ] =
50
+ useState < ScreenOrientation . Orientation > ( ) ;
45
51
46
52
useEffect ( ( ) => {
47
53
async function prepare ( ) {
54
+ // Set initial orientation.
55
+ const curOrientation = await ScreenOrientation . getOrientationAsync ( ) ;
56
+ setOrientation ( curOrientation ) ;
57
+
58
+ // Listens to orientation change.
59
+ ScreenOrientation . addOrientationChangeListener ( ( event ) => {
60
+ setOrientation ( event . orientationInfo . orientation ) ;
61
+ } ) ;
62
+
48
63
// Camera permission.
49
64
await Camera . requestPermissionsAsync ( ) ;
50
65
@@ -107,13 +122,19 @@ export default function App() {
107
122
. filter ( ( k ) => ( k . score ?? 0 ) > MIN_KEYPOINT_SCORE )
108
123
. map ( ( k ) => {
109
124
// Flip horizontally on android.
110
- const x = Platform . OS === 'android' ? OUTPUT_TENSOR_WIDTH - k . x : k . x ;
125
+ const x = IS_ANDROID ? OUTPUT_TENSOR_WIDTH - k . x : k . x ;
111
126
const y = k . y ;
127
+ const cx =
128
+ ( x / getOutputTensorWidth ( ) ) *
129
+ ( isPortrait ( ) ? CAM_PREVIEW_WIDTH : CAM_PREVIEW_HEIGHT ) ;
130
+ const cy =
131
+ ( y / getOutputTensorHeight ( ) ) *
132
+ ( isPortrait ( ) ? CAM_PREVIEW_HEIGHT : CAM_PREVIEW_WIDTH ) ;
112
133
return (
113
134
< Circle
114
135
key = { `skeletonkp_${ k . name } ` }
115
- cx = { ( x / OUTPUT_TENSOR_WIDTH ) * CAM_PREVIEW_WIDTH }
116
- cy = { ( y / OUTPUT_TENSOR_HEIGHT ) * CAM_PREVIEW_HEIGHT }
136
+ cx = { cx }
137
+ cy = { cy }
117
138
r = '4'
118
139
strokeWidth = '2'
119
140
fill = '#00AA00'
@@ -136,6 +157,53 @@ export default function App() {
136
157
) ;
137
158
} ;
138
159
160
+ const isPortrait = ( ) => {
161
+ return (
162
+ orientation === ScreenOrientation . Orientation . PORTRAIT_UP ||
163
+ orientation === ScreenOrientation . Orientation . PORTRAIT_DOWN
164
+ ) ;
165
+ } ;
166
+
167
+ const getOutputTensorWidth = ( ) => {
168
+ // On iOS landscape mode, switch width and height of the output tensor to
169
+ // get better result. Without this, the image stored in the output tensor
170
+ // would be stretched too much.
171
+ //
172
+ // Same for getOutputTensorHeight below.
173
+ return isPortrait ( ) || IS_ANDROID
174
+ ? OUTPUT_TENSOR_WIDTH
175
+ : OUTPUT_TENSOR_HEIGHT ;
176
+ } ;
177
+
178
+ const getOutputTensorHeight = ( ) => {
179
+ return isPortrait ( ) || IS_ANDROID
180
+ ? OUTPUT_TENSOR_HEIGHT
181
+ : OUTPUT_TENSOR_WIDTH ;
182
+ } ;
183
+
184
+ const getTextureRotationAngleInDegrees = ( ) => {
185
+ // On Android, the camera texture will rotate behind the scene as the phone
186
+ // changes orientation, so we don't need to rotate it in TensorCamera.
187
+ if ( IS_ANDROID ) {
188
+ return 0 ;
189
+ }
190
+
191
+ // For iOS, the camera texture won't rotate automatically. Calculate the
192
+ // rotation angles here which will be passed to TensorCamera to rotate it
193
+ // internally.
194
+ switch ( orientation ) {
195
+ // Not supported on iOS as of 11/2021, but add it here just in case.
196
+ case ScreenOrientation . Orientation . PORTRAIT_DOWN :
197
+ return 180 ;
198
+ case ScreenOrientation . Orientation . LANDSCAPE_LEFT :
199
+ return 270 ;
200
+ case ScreenOrientation . Orientation . LANDSCAPE_RIGHT :
201
+ return 90 ;
202
+ default :
203
+ return 0 ;
204
+ }
205
+ } ;
206
+
139
207
if ( ! tfReady ) {
140
208
return (
141
209
< View style = { styles . loadingMsg } >
@@ -146,16 +214,21 @@ export default function App() {
146
214
return (
147
215
// Note that you don't need to specify `cameraTextureWidth` and
148
216
// `cameraTextureHeight` prop in `TensorCamera` below.
149
- < View style = { styles . container } >
217
+ < View
218
+ style = {
219
+ isPortrait ( ) ? styles . containerPortrait : styles . containerLandscape
220
+ }
221
+ >
150
222
< TensorCamera
151
223
ref = { cameraRef }
152
224
style = { styles . camera }
153
225
autorender = { AUTO_RENDER }
154
226
type = { Camera . Constants . Type . front }
155
227
// tensor related props
156
- resizeWidth = { OUTPUT_TENSOR_WIDTH }
157
- resizeHeight = { OUTPUT_TENSOR_HEIGHT }
228
+ resizeWidth = { getOutputTensorWidth ( ) }
229
+ resizeHeight = { getOutputTensorHeight ( ) }
158
230
resizeDepth = { 3 }
231
+ rotation = { getTextureRotationAngleInDegrees ( ) }
159
232
onReady = { handleCameraStream }
160
233
/>
161
234
{ renderPose ( ) }
@@ -166,8 +239,17 @@ export default function App() {
166
239
}
167
240
168
241
const styles = StyleSheet . create ( {
169
- container : {
242
+ containerPortrait : {
243
+ position : 'relative' ,
244
+ width : CAM_PREVIEW_WIDTH ,
245
+ height : CAM_PREVIEW_HEIGHT ,
246
+ marginTop : Dimensions . get ( 'window' ) . height / 2 - CAM_PREVIEW_HEIGHT / 2 ,
247
+ } ,
248
+ containerLandscape : {
170
249
position : 'relative' ,
250
+ width : CAM_PREVIEW_HEIGHT ,
251
+ height : CAM_PREVIEW_WIDTH ,
252
+ marginLeft : Dimensions . get ( 'window' ) . height / 2 - CAM_PREVIEW_HEIGHT / 2 ,
171
253
} ,
172
254
loadingMsg : {
173
255
position : 'absolute' ,
@@ -177,24 +259,19 @@ const styles = StyleSheet.create({
177
259
justifyContent : 'center' ,
178
260
} ,
179
261
camera : {
180
- position : 'absolute' ,
181
- top : Dimensions . get ( 'window' ) . height / 2 - CAM_PREVIEW_HEIGHT / 2 ,
182
- left : Dimensions . get ( 'window' ) . width / 2 - CAM_PREVIEW_WIDTH / 2 ,
183
- width : CAM_PREVIEW_WIDTH ,
184
- height : CAM_PREVIEW_HEIGHT ,
262
+ width : '100%' ,
263
+ height : '100%' ,
185
264
zIndex : 1 ,
186
265
} ,
187
266
svg : {
188
- top : Dimensions . get ( 'window' ) . height / 2 - CAM_PREVIEW_HEIGHT / 2 ,
189
- left : Dimensions . get ( 'window' ) . width / 2 - CAM_PREVIEW_WIDTH / 2 ,
190
- width : CAM_PREVIEW_WIDTH ,
191
- height : CAM_PREVIEW_HEIGHT ,
267
+ width : '100%' ,
268
+ height : '100%' ,
192
269
position : 'absolute' ,
193
270
zIndex : 30 ,
194
271
} ,
195
272
fpsContainer : {
196
273
position : 'absolute' ,
197
- top : 80 ,
274
+ top : 10 ,
198
275
left : 10 ,
199
276
width : 80 ,
200
277
alignItems : 'center' ,
0 commit comments