-
Notifications
You must be signed in to change notification settings - Fork 2k
/
Copy pathcamera_stream.tsx
425 lines (393 loc) · 14.1 KB
/
camera_stream.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
/**
* @license
* Copyright 2020 Google LLC. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* =============================================================================
*/
import * as React from 'react';
import * as tf from '@tensorflow/tfjs-core';
import {
StyleSheet,
PixelRatio,
LayoutChangeEvent,
Platform,
} from 'react-native';
import { Camera } from 'expo-camera';
import { GLView, ExpoWebGLRenderingContext } from 'expo-gl';
import { fromTexture, renderToGLView, detectGLCapabilities } from './camera';
import { Rotation } from './types';
interface WrappedComponentProps {
onLayout?: (event: LayoutChangeEvent) => void;
// tslint:disable-next-line: no-any
[index: string]: any;
}
interface Props {
useCustomShadersToResize: boolean;
cameraTextureWidth: number;
cameraTextureHeight: number;
resizeWidth: number;
resizeHeight: number;
resizeDepth: number;
autorender: boolean;
rotation?: Rotation;
onReady: (
images: IterableIterator<tf.Tensor3D>,
updateCameraPreview: () => void,
gl: ExpoWebGLRenderingContext,
cameraTexture: WebGLTexture
) => void;
}
interface State {
cameraLayout: { x: number; y: number; width: number; height: number };
}
const DEFAULT_AUTORENDER = true;
const DEFAULT_RESIZE_DEPTH = 3;
const DEFAULT_USE_CUSTOM_SHADERS_TO_RESIZE = false;
/**
* A higher-order-component (HOC) that augments the [Expo.Camera](https://docs.expo.io/versions/latest/sdk/camera/)
* component with the ability to yield tensors representing the camera stream.
*
* Because the camera data will be consumed in the process, the original
* camera component will not render any content. This component provides
* options that can be used to render the camera preview.
*
* Notably the component allows on-the-fly resizing of the camera image to
* smaller dimensions, this speeds up data transfer between the native and
* javascript threads immensely.
*
* __In addition to__ all the props taken by Expo.Camera. The returned
* component takes the following props
*
* - __use_custom_shaders_to_resize__: boolean — whether to use custom shaders
* to resize the camera image to smaller dimensions that fit the output
* tensor.
* - If it is set to false (default and recommended), the resize will be done
* by the underlying GL system when drawing the camera image texture to the
* target output texture with TEXTURE_MIN_FILTER/TEXTURE_MAG_FILTER set to
* gl.LINEAR, and there is no need to provide `cameraTextureWidth` and
* `cameraTextureHeight` props below.
* - If it is set to true (legacy), the resize will be done by the custom
* shaders defined in `resize_bilinear_program_info.ts`. Setting it to true
* also requires that client provide the correct `cameraTextureWidth` and
* `cameraTextureHeight` props below. Unfortunately there is no official API
* to get the camera texture size programmatically so they have to be
* decided empirically. From our experience, it is hard to cover all cases
* in this way because different devices models and/or preview sizes might
* produce different camera texture sizes.
* - __cameraTextureWidth__: number — the width the camera preview texture
* (see note above)
* - __cameraTextureHeight__: number — the height the camera preview texture
* (see note above)
* - __resizeWidth__: number — the width of the output tensor
* - __resizeHeight__: number — the height of the output tensor
* - __resizeDepth__: number — the depth (num of channels) of the output tensor.
* Should be 3 or 4.
* - __autorender__: boolean — if true the view will be automatically updated
* with the contents of the camera. Set this to false if you want more direct
* control on when rendering happens.
* - __rotation__: number — the degrees that the internal camera texture and
* preview will be rotated.
* - __onReady__: (
* images: IterableIterator<tf.Tensor3D>,
* updateCameraPreview: () => void,
* gl: ExpoWebGLRenderingContext,
* cameraTexture: WebGLTexture
* ) => void — When the component is mounted and ready this callback will
* be called and recieve the following 3 elements:
* - __images__ is a (iterator)[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators]
* that yields tensors representing the camera image on demand.
* - __updateCameraPreview__ is a function that will update the WebGL render
* buffer with the contents of the camera. Not needed when `autorender`
* is true
* - __gl__ is the ExpoWebGl context used to do the rendering. After calling
* `updateCameraPreview` and any other operations you want to synchronize
* to the camera rendering you must call gl.endFrameExp() to display it
* on the screen. This is also provided in case you want to do other
* rendering using WebGL. Not needed when `autorender` is true.
* - __cameraTexture__ The underlying cameraTexture. This can be used to
* implement your own __updateCameraPreview__.
*
* ```js
* import { Camera } from 'expo-camera';
* import { cameraWithTensors } from '@tensorflow/tfjs-react-native';
*
* const TensorCamera = cameraWithTensors(Camera);
*
* class MyComponent {
*
* handleCameraStream(images, updatePreview, gl) {
* const loop = async () => {
* const nextImageTensor = images.next().value
*
* //
* // do something with tensor here
* //
*
* // if autorender is false you need the following two lines.
* // updatePreview();
* // gl.endFrameEXP();
*
* requestAnimationFrame(loop);
* }
* loop();
* }
*
* render() {
* return <View>
* <TensorCamera
* // Standard Camera props
* style={styles.camera}
* type={Camera.Constants.Type.front}
* // Tensor related props
* resizeHeight={200}
* resizeWidth={152}
* resizeDepth={3}
* onReady={this.handleCameraStream}
* autorender={true}
* />
* </View>
* }
* }
* ```
*
* @param CameraComponent an expo Camera component constructor
*/
/** @doc {heading: 'Media', subheading: 'Camera'} */
export function cameraWithTensors<T extends WrappedComponentProps>(
// tslint:disable-next-line: variable-name
CameraComponent: React.ComponentType<T>
) {
return class CameraWithTensorStream extends React.Component<
T & Props,
State
> {
camera: Camera;
glView: GLView;
glContext: ExpoWebGLRenderingContext;
rafID: number;
constructor(props: T & Props) {
super(props);
this.onCameraLayout = this.onCameraLayout.bind(this);
this.onGLContextCreate = this.onGLContextCreate.bind(this);
this.state = {
cameraLayout: null,
};
}
componentWillUnmount() {
cancelAnimationFrame(this.rafID);
if (this.glContext) {
GLView.destroyContextAsync(this.glContext);
}
this.camera = null;
this.glView = null;
this.glContext = null;
}
/*
* Measure the camera component when it is laid out so that we can overlay
* the GLView.
*/
onCameraLayout(event: LayoutChangeEvent) {
const { x, y, width, height } = event.nativeEvent.layout;
this.setState({
cameraLayout: { x, y, width, height },
});
}
/**
* Creates a WebGL texture that is updated by the underlying platform to
* contain the contents of the camera.
*/
async createCameraTexture(): Promise<WebGLTexture> {
if (this.glView != null && this.camera != null) {
//@ts-ignore
return this.glView.createCameraTextureAsync(this.camera);
} else {
throw new Error('Expo GL context or camera not available');
}
}
/**
* Callback for GL context creation. We do mose of the work of setting
* up the component here.
* @param gl
*/
async onGLContextCreate(gl: ExpoWebGLRenderingContext) {
this.glContext = gl;
const cameraTexture = await this.createCameraTexture();
await detectGLCapabilities(gl);
// Optionally set up a render loop that just displays the camera texture
// to the GLView.
const autorender =
this.props.autorender != null
? this.props.autorender
: DEFAULT_AUTORENDER;
const updatePreview = this.previewUpdateFunc(gl, cameraTexture);
if (autorender) {
const renderLoop = () => {
updatePreview();
gl.endFrameEXP();
this.rafID = requestAnimationFrame(renderLoop);
};
renderLoop();
}
const { resizeDepth } = this.props;
// cameraTextureHeight and cameraTextureWidth props can be omitted when
// useCustomShadersToResize is set to false. Setting a default value to
// them here.
const cameraTextureHeight =
this.props.cameraTextureHeight != null
? this.props.cameraTextureHeight
: 0;
const cameraTextureWidth =
this.props.cameraTextureWidth != null
? this.props.cameraTextureWidth
: 0;
const useCustomShadersToResize =
this.props.useCustomShadersToResize != null
? this.props.useCustomShadersToResize
: DEFAULT_USE_CUSTOM_SHADERS_TO_RESIZE;
//
// Set up a generator function that yields tensors representing the
// camera on demand.
//
const cameraStreamView = this;
function* nextFrameGenerator() {
const RGBA_DEPTH = 4;
const textureDims = {
height: cameraTextureHeight,
width: cameraTextureWidth,
depth: RGBA_DEPTH,
};
while (cameraStreamView.glContext != null) {
const targetDims = {
height: cameraStreamView.props.resizeHeight,
width: cameraStreamView.props.resizeWidth,
depth: resizeDepth || DEFAULT_RESIZE_DEPTH,
};
const imageTensor = fromTexture(
gl,
cameraTexture,
textureDims,
targetDims,
useCustomShadersToResize,
{ rotation: cameraStreamView.props.rotation }
);
yield imageTensor;
}
}
const nextFrameIterator = nextFrameGenerator();
// Pass the utility functions to the caller provided callback
this.props.onReady(nextFrameIterator, updatePreview, gl, cameraTexture);
}
/**
* Helper function that can be used to update the GLView framebuffer.
*
* @param gl the open gl texture to render to
* @param cameraTexture the texture to draw.
*/
previewUpdateFunc(
gl: ExpoWebGLRenderingContext,
cameraTexture: WebGLTexture
) {
const renderFunc = () => {
const { cameraLayout } = this.state;
const { rotation } = this.props;
const width = PixelRatio.getPixelSizeForLayoutSize(cameraLayout.width);
const height = PixelRatio.getPixelSizeForLayoutSize(
cameraLayout.height
);
const isFrontCamera =
this.camera.props.type === Camera.Constants.Type.front;
const flipHorizontal =
Platform.OS === 'ios' && isFrontCamera ? false : true;
renderToGLView(
gl,
cameraTexture,
{ width, height },
flipHorizontal,
rotation
);
};
return renderFunc.bind(this);
}
/**
* Render the component
*/
render() {
const { cameraLayout } = this.state;
// Before passing props into the original wrapped component we want to
// remove the props that we augment the component with.
// Use this object to use typescript to check that we are removing
// all the tensorCamera properties.
const tensorCameraPropMap: Props = {
useCustomShadersToResize: null,
cameraTextureWidth: null,
cameraTextureHeight: null,
resizeWidth: null,
resizeHeight: null,
resizeDepth: null,
autorender: null,
onReady: null,
rotation: 0,
};
const tensorCameraPropKeys = Object.keys(tensorCameraPropMap);
const cameraProps: WrappedComponentProps = {};
const allProps = Object.keys(this.props);
for (let i = 0; i < allProps.length; i++) {
const key = allProps[i];
if (!tensorCameraPropKeys.includes(key)) {
cameraProps[key] = this.props[key];
}
}
// Set up an on layout handler
const onlayout = this.props.onLayout
? (e: LayoutChangeEvent) => {
this.props.onLayout(e);
this.onCameraLayout(e);
}
: this.onCameraLayout;
cameraProps.onLayout = onlayout;
const cameraComp = (
//@ts-ignore see https://github.com/microsoft/TypeScript/issues/30650
<CameraComponent
key='camera-with-tensor-camera-view'
{...cameraProps}
ref={(ref: Camera) => (this.camera = ref)}
/>
);
// Create the glView if the camera has mounted.
let glViewComponent = null;
if (cameraLayout != null) {
const styles = StyleSheet.create({
glView: {
position: 'absolute',
left: cameraLayout.x,
top: cameraLayout.y,
width: cameraLayout.width,
height: cameraLayout.height,
zIndex: this.props.style.zIndex
? parseInt(this.props.style.zIndex, 10) + 10
: 10,
},
});
glViewComponent = (
<GLView
key='camera-with-tensor-gl-view'
style={styles.glView}
onContextCreate={this.onGLContextCreate}
ref={(ref) => (this.glView = ref)}
/>
);
}
return [cameraComp, glViewComponent];
}
};
}