Skip to content

Commit b741bfc

Browse files
Update pose-detection demo to support orientation change (#629)
* Update pose-detection demo to support orientation change * update * fix * fix
1 parent 5c675ee commit b741bfc

11 files changed

+155
-49
lines changed

cloudbuild.yml

+14-14
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
steps:
2-
- name: 'node:10'
3-
entrypoint: 'yarn'
4-
id: 'yarn'
5-
args: ['install']
6-
- name: 'node:10'
7-
entrypoint: 'yarn'
8-
id: 'test'
9-
args: ['presubmit']
10-
waitFor: ['yarn']
11-
env: ['BROWSERSTACK_USERNAME=deeplearnjs1']
12-
secretEnv: ['BROWSERSTACK_KEY']
2+
- name: 'node:12'
3+
entrypoint: 'yarn'
4+
id: 'yarn'
5+
args: ['install']
6+
- name: 'node:12'
7+
entrypoint: 'yarn'
8+
id: 'test'
9+
args: ['presubmit']
10+
waitFor: ['yarn']
11+
env: ['BROWSERSTACK_USERNAME=deeplearnjs1']
12+
secretEnv: ['BROWSERSTACK_KEY']
1313
secrets:
14-
- kmsKeyName: projects/learnjs-174218/locations/global/keyRings/tfjs/cryptoKeys/enc
15-
secretEnv:
16-
BROWSERSTACK_KEY: CiQAkwyoIW0LcnxymzotLwaH4udVTQFBEN4AEA5CA+a3+yflL2ASPQAD8BdZnGARf78MhH5T9rQqyz9HNODwVjVIj64CTkFlUCGrP1B2HX9LXHWHLmtKutEGTeFFX9XhuBzNExA=
14+
- kmsKeyName: projects/learnjs-174218/locations/global/keyRings/tfjs/cryptoKeys/enc
15+
secretEnv:
16+
BROWSERSTACK_KEY: CiQAkwyoIW0LcnxymzotLwaH4udVTQFBEN4AEA5CA+a3+yflL2ASPQAD8BdZnGARf78MhH5T9rQqyz9HNODwVjVIj64CTkFlUCGrP1B2HX9LXHWHLmtKutEGTeFFX9XhuBzNExA=
1717
timeout: 1800s
1818
logsBucket: 'gs://tfjs-build-logs'
1919
options:

presubmit.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ dirs.forEach(dir => {
3838
return;
3939
}
4040

41-
const packageJSON: {} =
41+
const packageJSON: {'scripts': {[key: string]: string}} =
4242
JSON.parse(fs.readFileSync('./package.json', {encoding: 'utf-8'}));
4343
if (packageJSON['scripts']['test'] != null) {
4444
console.log(`~~~~~~~~~~~~ Testing ${dir} ~~~~~~~~~~~~`);

react-native/pose-detection/App.tsx

+99-22
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ import { Camera } from 'expo-camera';
55

66
import * as tf from '@tensorflow/tfjs';
77
import * as posedetection from '@tensorflow-models/pose-detection';
8+
import * as ScreenOrientation from 'expo-screen-orientation';
89
import { cameraWithTensors } from '@tensorflow/tfjs-react-native';
910
import Svg, { Circle } from 'react-native-svg';
1011
import { ExpoWebGLRenderingContext } from 'expo-gl';
1112

1213
// tslint:disable-next-line: variable-name
1314
const TensorCamera = cameraWithTensors(Camera);
1415

16+
const IS_ANDROID = Platform.OS === 'android';
17+
const IS_IOS = Platform.OS === 'ios';
18+
1519
// Camera preview size.
1620
//
1721
// From experiments, to render camera feed without distortion, 16:9 ratio
@@ -20,18 +24,18 @@ const TensorCamera = cameraWithTensors(Camera);
2024
//
2125
// This might not cover all cases.
2226
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);
2528

2629
// The score threshold for pose detection results.
2730
const MIN_KEYPOINT_SCORE = 0.3;
2831

2932
// The size of the resized output from TensorCamera.
3033
//
3134
// 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);
3539

3640
// Whether to auto-render TensorCamera preview.
3741
const AUTO_RENDER = false;
@@ -42,9 +46,20 @@ export default function App() {
4246
const [model, setModel] = useState<posedetection.PoseDetector>();
4347
const [poses, setPoses] = useState<posedetection.Pose[]>();
4448
const [fps, setFps] = useState(0);
49+
const [orientation, setOrientation] =
50+
useState<ScreenOrientation.Orientation>();
4551

4652
useEffect(() => {
4753
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+
4863
// Camera permission.
4964
await Camera.requestPermissionsAsync();
5065

@@ -107,13 +122,19 @@ export default function App() {
107122
.filter((k) => (k.score ?? 0) > MIN_KEYPOINT_SCORE)
108123
.map((k) => {
109124
// 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;
111126
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);
112133
return (
113134
<Circle
114135
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}
117138
r='4'
118139
strokeWidth='2'
119140
fill='#00AA00'
@@ -136,6 +157,53 @@ export default function App() {
136157
);
137158
};
138159

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+
139207
if (!tfReady) {
140208
return (
141209
<View style={styles.loadingMsg}>
@@ -146,16 +214,21 @@ export default function App() {
146214
return (
147215
// Note that you don't need to specify `cameraTextureWidth` and
148216
// `cameraTextureHeight` prop in `TensorCamera` below.
149-
<View style={styles.container}>
217+
<View
218+
style={
219+
isPortrait() ? styles.containerPortrait : styles.containerLandscape
220+
}
221+
>
150222
<TensorCamera
151223
ref={cameraRef}
152224
style={styles.camera}
153225
autorender={AUTO_RENDER}
154226
type={Camera.Constants.Type.front}
155227
// tensor related props
156-
resizeWidth={OUTPUT_TENSOR_WIDTH}
157-
resizeHeight={OUTPUT_TENSOR_HEIGHT}
228+
resizeWidth={getOutputTensorWidth()}
229+
resizeHeight={getOutputTensorHeight()}
158230
resizeDepth={3}
231+
rotation={getTextureRotationAngleInDegrees()}
159232
onReady={handleCameraStream}
160233
/>
161234
{renderPose()}
@@ -166,8 +239,17 @@ export default function App() {
166239
}
167240

168241
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: {
170249
position: 'relative',
250+
width: CAM_PREVIEW_HEIGHT,
251+
height: CAM_PREVIEW_WIDTH,
252+
marginLeft: Dimensions.get('window').height / 2 - CAM_PREVIEW_HEIGHT / 2,
171253
},
172254
loadingMsg: {
173255
position: 'absolute',
@@ -177,24 +259,19 @@ const styles = StyleSheet.create({
177259
justifyContent: 'center',
178260
},
179261
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%',
185264
zIndex: 1,
186265
},
187266
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%',
192269
position: 'absolute',
193270
zIndex: 30,
194271
},
195272
fpsContainer: {
196273
position: 'absolute',
197-
top: 80,
274+
top: 10,
198275
left: 10,
199276
width: 80,
200277
alignItems: 'center',

react-native/pose-detection/README.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
A quick demo for running [TFJS Pose Detection][posedetection] model
44
([MoveNet.SinglePose.Ligntning][tfhub]) using
5-
[TFJS React Native][tfjs-react-native] in an Expo project. Only the keypoints
6-
are rendered in the demo.
5+
[TFJS React Native][tfjs-react-native] in an Expo project. It supports both
6+
portrait and landscape mode. Only the keypoints are rendered in the demo.
77

88
To run it locally:
99

@@ -15,7 +15,9 @@ $ yarn start
1515
Then scan the QR code to open it in the `Expo Go` app.
1616

1717
Tested on Pixel 2, iPhone 12 Pro Max, iPad, and iPad Pro.
18-
<img src="screenshot.jpg" width="300">
18+
19+
<img src="screenshot_portrait.jpg" width="250">
20+
<img src="screenshot_landscape.jpg" width="500">
1921

2022
[posedetection]: https://github.com/tensorflow/tfjs-models/tree/master/pose-detection
2123
[tfhub]: https://tfhub.dev/google/tfjs-model/movenet/singlepose/lightning/4

react-native/pose-detection/ios/Podfile.lock

+23-7
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,9 @@ PODS:
33
- DoubleConversion (1.1.6)
44
- EXApplication (3.1.2):
55
- UMCore
6-
- EXCamera (7.0.0):
7-
- UMBarCodeScannerInterface
6+
- EXCamera (11.2.2):
7+
- ExpoModulesCore
88
- UMCore
9-
- UMFaceDetectorInterface
10-
- UMFileSystemInterface
11-
- UMImageLoaderInterface
12-
- UMPermissionsInterface
139
- EXConstants (10.1.3):
1410
- UMConstantsInterface
1511
- UMCore
@@ -42,6 +38,18 @@ PODS:
4238
- EXPermissions (12.0.1):
4339
- UMCore
4440
- UMPermissionsInterface
41+
- ExpoModulesCore (0.2.0):
42+
- ExpoModulesCore/Core (= 0.2.0)
43+
- ExpoModulesCore/Interfaces (= 0.2.0)
44+
- UMCore
45+
- ExpoModulesCore/Core (0.2.0):
46+
- UMCore
47+
- ExpoModulesCore/Interfaces (0.2.0):
48+
- ExpoModulesCore/Core
49+
- UMCore
50+
- EXScreenOrientation (3.1.0):
51+
- React-Core
52+
- UMCore
4553
- EXSplashScreen (0.10.3):
4654
- React-Core
4755
- UMCore
@@ -372,6 +380,8 @@ DEPENDENCIES:
372380
- EXImageManipulator (from `../node_modules/expo-image-manipulator/ios`)
373381
- EXKeepAwake (from `../node_modules/expo-keep-awake/ios`)
374382
- EXPermissions (from `../node_modules/expo-permissions/ios`)
383+
- ExpoModulesCore (from `../node_modules/expo-modules-core/ios`)
384+
- EXScreenOrientation (from `../node_modules/expo-screen-orientation/ios`)
375385
- EXSplashScreen (from `../node_modules/expo-splash-screen/ios`)
376386
- EXStructuredHeaders (from `../node_modules/expo-structured-headers/ios`)
377387
- EXUpdates (from `../node_modules/expo-updates/ios`)
@@ -453,6 +463,10 @@ EXTERNAL SOURCES:
453463
:path: "../node_modules/expo-keep-awake/ios"
454464
EXPermissions:
455465
:path: "../node_modules/expo-permissions/ios"
466+
ExpoModulesCore:
467+
:path: "../node_modules/expo-modules-core/ios"
468+
EXScreenOrientation:
469+
:path: "../node_modules/expo-screen-orientation/ios"
456470
EXSplashScreen:
457471
:path: "../node_modules/expo-splash-screen/ios"
458472
EXStructuredHeaders:
@@ -552,7 +566,7 @@ SPEC CHECKSUMS:
552566
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
553567
DoubleConversion: cde416483dac037923206447da6e1454df403714
554568
EXApplication: 4797b8b37f0b0470f587fdccf6407f44b50d18b5
555-
EXCamera: 8ad7012d4e0fef09ff35de14fcb41b410357e9be
569+
EXCamera: 042b334f6ee9b918855fe4c4f155d4c372300a5f
556570
EXConstants: c4dd28acc12039c999612507a5f935556f2c86ce
557571
EXErrorRecovery: 720641265b8cf95e6cdeb1884ac38e794a352488
558572
EXFileSystem: dcf2273f49431e5037347c733a2dc5d08e0d0a9e
@@ -563,6 +577,8 @@ SPEC CHECKSUMS:
563577
EXImageManipulator: b2e17bdbd0872cc59b43b76756f76fdb8a849831
564578
EXKeepAwake: d4e4a3ed8c1c4fd940dd62fc5a8be2a190371fd4
565579
EXPermissions: 8f8c1c05580c4e02d4ee2c8dd74bfe173ff6a723
580+
ExpoModulesCore: 2734852616127a6c1fc23012197890a6f3763dc7
581+
EXScreenOrientation: 4d9ec57c73055c8a4dbf9dd85def4db29b690a91
566582
EXSplashScreen: 6d5b4274537d2b9f343a9ec2347dbb61f3d40ab7
567583
EXStructuredHeaders: be834496a4d9fd0069cd12ec1cc57b31c6d3b256
568584
EXUpdates: efe0e8c514dcff06a8fd0b63be6019a6365fb9c7

react-native/pose-detection/ios/posenet/AppDelegate.m

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
1919
#import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
2020
#import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>
21+
#import <EXScreenOrientation/EXScreenOrientationViewController.h>
2122

2223
static void InitializeFlipper(UIApplication *application) {
2324
FlipperClient *client = [FlipperClient sharedClient];
@@ -67,7 +68,7 @@ - (RCTBridge *)initializeReactNativeApp
6768
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"main" initialProperties:nil];
6869
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
6970

70-
UIViewController *rootViewController = [UIViewController new];
71+
UIViewController *rootViewController = [[EXScreenOrientationViewController alloc] init]; // The default screen orientation will be set to `portrait`.
7172
rootViewController.view = rootView;
7273
self.window.rootViewController = rootViewController;
7374
[self.window makeKeyAndVisible];

react-native/pose-detection/package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,23 @@
44
"version": "0.0.1",
55
"license": "Apache-2.0",
66
"scripts": {
7+
"build-dep": "cd ../../../tfjs/tfjs-react-native && yarn build-npm",
8+
"copy-dep": "rm -rf node_modules/@tensorflow/tfjs-react-native/dist/* && cp -rf ../../../tfjs/tfjs-react-native/dist/* node_modules/@tensorflow/tfjs-react-native/dist/",
79
"start": "expo start"
810
},
911
"dependencies": {
1012
"@mediapipe/pose": "~0.4.0",
1113
"@react-native-async-storage/async-storage": "1.15.7",
1214
"@tensorflow-models/pose-detection": "0.0.6",
1315
"@tensorflow/tfjs": "3.9.0",
14-
"@tensorflow/tfjs-react-native": "0.7.0",
16+
"@tensorflow/tfjs-react-native": "0.8.0",
1517
"expo": "~41.0.1",
1618
"expo-camera": "^11.2.2",
1719
"expo-file-system": "^10.0.0",
1820
"expo-gl": "^7.0.0",
1921
"expo-gl-cpp": "^7.0.0",
2022
"expo-image-manipulator": "^6.0.0",
23+
"expo-screen-orientation": "~3.1.0",
2124
"expo-splash-screen": "~0.10.2",
2225
"expo-updates": "~0.5.4",
2326
"react": "16.13.1",
-185 KB
Binary file not shown.
Loading
Loading

0 commit comments

Comments
 (0)