Skip to content

Commit 54bf277

Browse files
author
Nikhil Thorat
authored
[tfjs-core] Fix browser.fromPixels to work without needing read… (#1947)
BUG This now checks the natural video width for videos as well as removing the check for document.readyState (which is not needed, verified in a local client). This also fixes a bug where if the video isn't ready the user gets an opaque error about [0x0] textures, this throws a friendly error about waiting for the right event on the video tag.
1 parent 8d155c8 commit 54bf277

File tree

5 files changed

+82
-27
lines changed

5 files changed

+82
-27
lines changed

tfjs-core/src/backends/cpu/backend_cpu.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,12 @@ export class MathBackendCPU implements KernelBackend {
142142
pixels instanceof HTMLVideoElement;
143143
const isImage = typeof (HTMLImageElement) !== 'undefined' &&
144144
pixels instanceof HTMLImageElement;
145-
145+
const [width, height] = isVideo ?
146+
[
147+
(pixels as HTMLVideoElement).videoWidth,
148+
(pixels as HTMLVideoElement).videoHeight
149+
] :
150+
[pixels.width, pixels.height];
146151
let vals: Uint8ClampedArray|Uint8Array;
147152
// tslint:disable-next-line:no-any
148153
if (ENV.get('IS_NODE') && (pixels as any).getContext == null) {
@@ -155,7 +160,7 @@ export class MathBackendCPU implements KernelBackend {
155160
// tslint:disable-next-line:no-any
156161
vals = (pixels as any)
157162
.getContext('2d')
158-
.getImageData(0, 0, pixels.width, pixels.height)
163+
.getImageData(0, 0, width, height)
159164
.data;
160165
} else if (isImageData || isPixelData) {
161166
vals = (pixels as PixelData | ImageData).data;
@@ -165,13 +170,11 @@ export class MathBackendCPU implements KernelBackend {
165170
'Can\'t read pixels from HTMLImageElement outside ' +
166171
'the browser.');
167172
}
168-
this.fromPixels2DContext.canvas.width = pixels.width;
169-
this.fromPixels2DContext.canvas.height = pixels.height;
173+
this.fromPixels2DContext.canvas.width = width;
174+
this.fromPixels2DContext.canvas.height = height;
170175
this.fromPixels2DContext.drawImage(
171-
pixels as HTMLVideoElement, 0, 0, pixels.width, pixels.height);
172-
vals = this.fromPixels2DContext
173-
.getImageData(0, 0, pixels.width, pixels.height)
174-
.data;
176+
pixels as HTMLVideoElement, 0, 0, width, height);
177+
vals = this.fromPixels2DContext.getImageData(0, 0, width, height).data;
175178
} else {
176179
throw new Error(
177180
'pixels passed to tf.browser.fromPixels() must be either an ' +
@@ -183,16 +186,15 @@ export class MathBackendCPU implements KernelBackend {
183186
if (numChannels === 4) {
184187
values = new Int32Array(vals);
185188
} else {
186-
const numPixels = pixels.width * pixels.height;
189+
const numPixels = width * height;
187190
values = new Int32Array(numPixels * numChannels);
188191
for (let i = 0; i < numPixels; i++) {
189192
for (let channel = 0; channel < numChannels; ++channel) {
190193
values[i * numChannels + channel] = vals[i * 4 + channel];
191194
}
192195
}
193196
}
194-
const outShape: [number, number, number] =
195-
[pixels.height, pixels.width, numChannels];
197+
const outShape: [number, number, number] = [height, width, numChannels];
196198
return tensor3d(values, outShape, 'int32');
197199
}
198200
async read(dataId: DataId): Promise<BackendValues> {

tfjs-core/src/backends/webgl/backend_webgl.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -286,8 +286,6 @@ export class MathBackendWebGL implements KernelBackend {
286286
throw new Error(
287287
'pixels passed to tf.browser.fromPixels() can not be null');
288288
}
289-
const texShape: [number, number] = [pixels.height, pixels.width];
290-
const outShape = [pixels.height, pixels.width, numChannels];
291289

292290
const isCanvas = (typeof (OffscreenCanvas) !== 'undefined' &&
293291
pixels instanceof OffscreenCanvas) ||
@@ -300,6 +298,15 @@ export class MathBackendWebGL implements KernelBackend {
300298
pixels instanceof HTMLVideoElement;
301299
const isImage = typeof (HTMLImageElement) !== 'undefined' &&
302300
pixels instanceof HTMLImageElement;
301+
const [width, height] = isVideo ?
302+
[
303+
(pixels as HTMLVideoElement).videoWidth,
304+
(pixels as HTMLVideoElement).videoHeight
305+
] :
306+
[pixels.width, pixels.height];
307+
308+
const texShape: [number, number] = [height, width];
309+
const outShape = [height, width, numChannels];
303310

304311
if (!isCanvas && !isPixelData && !isImageData && !isVideo && !isImage) {
305312
throw new Error(
@@ -312,21 +319,15 @@ export class MathBackendWebGL implements KernelBackend {
312319

313320
if (isImage || isVideo) {
314321
if (this.fromPixels2DContext == null) {
315-
if (document.readyState !== 'complete') {
316-
throw new Error(
317-
'The DOM is not ready yet. Please call ' +
318-
'tf.browser.fromPixels() once the DOM is ready. One way to ' +
319-
'do that is to add an event listener for `load` ' +
320-
'on the document object');
321-
}
322322
//@ts-ignore
323323
this.fromPixels2DContext =
324324
createCanvas(ENV.getNumber('WEBGL_VERSION')).getContext('2d');
325325
}
326-
this.fromPixels2DContext.canvas.width = pixels.width;
327-
this.fromPixels2DContext.canvas.height = pixels.height;
326+
327+
this.fromPixels2DContext.canvas.width = width;
328+
this.fromPixels2DContext.canvas.height = height;
328329
this.fromPixels2DContext.drawImage(
329-
pixels as HTMLVideoElement, 0, 0, pixels.width, pixels.height);
330+
pixels as HTMLVideoElement | HTMLImageElement, 0, 0, width, height);
330331
//@ts-ignore
331332
pixels = this.fromPixels2DContext.canvas;
332333
}

tfjs-core/src/ops/array_ops_test.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1608,15 +1608,36 @@ describeWithFlags('fromPixels', BROWSER_ENVS, () => {
16081608
expect(data.length).toEqual(10 * 10 * 3);
16091609
});
16101610
it('fromPixels for HTMLVideolement', async () => {
1611+
const video = document.createElement('video');
1612+
video.autoplay = true;
1613+
const source = document.createElement('source');
1614+
// tslint:disable:max-line-length
1615+
source.src = 'data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAAu1tZGF0AAACrQYF//+p3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE1NSByMjkwMSA3ZDBmZjIyIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAxOCAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDExMyBtZT1oZXggc3VibWU9NyBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0xIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MSA4eDhkY3Q9MSBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0tMiB0aHJlYWRzPTMgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MSBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTEgb3Blbl9nb3A9MCB3ZWlnaHRwPTIga2V5aW50PTI1MCBrZXlpbnRfbWluPTEgc2NlbmVjdXQ9NDAgaW50cmFfcmVmcmVzaD0wIHJjX2xvb2thaGVhZD00MCByYz1jcmYgbWJ0cmVlPTEgY3JmPTI4LjAgcWNvbXA9MC42MCBxcG1pbj0wIHFwbWF4PTY5IHFwc3RlcD00IGlwX3JhdGlvPTEuNDAgYXE9MToxLjAwAIAAAAAwZYiEAD//8m+P5OXfBeLGOfKE3xkODvFZuBflHv/+VwJIta6cbpIo4ABLoKBaYTkTAAAC7m1vb3YAAABsbXZoZAAAAAAAAAAAAAAAAAAAA+gAAAPoAAEAAAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAIYdHJhawAAAFx0a2hkAAAAAwAAAAAAAAAAAAAAAQAAAAAAAAPoAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAACgAAAAWgAAAAAAJGVkdHMAAAAcZWxzdAAAAAAAAAABAAAD6AAAAAAAAQAAAAABkG1kaWEAAAAgbWRoZAAAAAAAAAAAAAAAAAAAQAAAAEAAVcQAAAAAAC1oZGxyAAAAAAAAAAB2aWRlAAAAAAAAAAAAAAAAVmlkZW9IYW5kbGVyAAAAATttaW5mAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAAD7c3RibAAAAJdzdHNkAAAAAAAAAAEAAACHYXZjMQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAACgAFoASAAAAEgAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABj//wAAADFhdmNDAWQACv/hABhnZAAKrNlCjfkhAAADAAEAAAMAAg8SJZYBAAZo6+JLIsAAAAAYc3R0cwAAAAAAAAABAAAAAQAAQAAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAEAAAABAAAAFHN0c3oAAAAAAAAC5QAAAAEAAAAUc3RjbwAAAAAAAAABAAAAMAAAAGJ1ZHRhAAAAWm1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAALWlsc3QAAAAlqXRvbwAAAB1kYXRhAAAAAQAAAABMYXZmNTguMTIuMTAw';
1616+
source.type = 'video/mp4';
1617+
video.appendChild(source);
1618+
document.body.appendChild(video);
1619+
1620+
// On mobile safari the ready state is ready immediately so we
1621+
if (video.readyState < 2) {
1622+
await new Promise(resolve => {
1623+
video.addEventListener('loadeddata', () => resolve());
1624+
});
1625+
}
1626+
1627+
const res = tf.browser.fromPixels(video);
1628+
expect(res.shape).toEqual([90, 160, 3]);
1629+
const data = await res.data();
1630+
expect(data.length).toEqual(90 * 160 *3);
1631+
document.body.removeChild(video);
1632+
});
1633+
1634+
it('fromPixels for HTMLVideolement throws without loadeddata', async () => {
16111635
const video = document.createElement('video');
16121636
video.width = 1;
16131637
video.height = 1;
16141638
video.src = 'data:image/gif;base64' +
16151639
',R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==';
1616-
const res = tf.browser.fromPixels(video);
1617-
expect(res.shape).toEqual([1, 1, 3]);
1618-
const data = await res.data();
1619-
expect(data.length).toEqual(1 * 1 * 3);
1640+
expect(() => tf.browser.fromPixels(video)).toThrowError();
16201641
});
16211642

16221643
it('throws when passed a primitive number', () => {

tfjs-core/src/ops/browser.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ function fromPixels_(
5252
throw new Error(
5353
'Cannot construct Tensor with more than 4 channels from pixels.');
5454
}
55+
const isVideo = typeof (HTMLVideoElement) !== 'undefined' &&
56+
pixels instanceof HTMLVideoElement;
57+
if (isVideo) {
58+
const HAVE_CURRENT_DATA_READY_STATE = 2;
59+
if (isVideo &&
60+
(pixels as HTMLVideoElement).readyState <
61+
HAVE_CURRENT_DATA_READY_STATE) {
62+
throw new Error(
63+
'The video element has not loaded data yet. Please wait for ' +
64+
'`loadeddata` event on the <video> element.');
65+
}
66+
}
5567
return ENGINE.fromPixels(pixels, numChannels);
5668
}
5769

tfjs-core/test.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
2+
<script src="dist/tf-core.js"></script>
3+
4+
<img id="imgz" src="https://i.imgur.com/qMms916.jpg" crossorigin="anonymous"/>
5+
6+
<video id="img" crossorigin="anonymous" autoplay="autoplay">
7+
<source src="data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAAu1tZGF0AAACrQYF//+p3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE1NSByMjkwMSA3ZDBmZjIyIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAxOCAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDExMyBtZT1oZXggc3VibWU9NyBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0xIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MSA4eDhkY3Q9MSBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0tMiB0aHJlYWRzPTMgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MSBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTEgb3Blbl9nb3A9MCB3ZWlnaHRwPTIga2V5aW50PTI1MCBrZXlpbnRfbWluPTEgc2NlbmVjdXQ9NDAgaW50cmFfcmVmcmVzaD0wIHJjX2xvb2thaGVhZD00MCByYz1jcmYgbWJ0cmVlPTEgY3JmPTI4LjAgcWNvbXA9MC42MCBxcG1pbj0wIHFwbWF4PTY5IHFwc3RlcD00IGlwX3JhdGlvPTEuNDAgYXE9MToxLjAwAIAAAAAwZYiEAD//8m+P5OXfBeLGOfKE3xkODvFZuBflHv/+VwJIta6cbpIo4ABLoKBaYTkTAAAC7m1vb3YAAABsbXZoZAAAAAAAAAAAAAAAAAAAA+gAAAPoAAEAAAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAIYdHJhawAAAFx0a2hkAAAAAwAAAAAAAAAAAAAAAQAAAAAAAAPoAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAACgAAAAWgAAAAAAJGVkdHMAAAAcZWxzdAAAAAAAAAABAAAD6AAAAAAAAQAAAAABkG1kaWEAAAAgbWRoZAAAAAAAAAAAAAAAAAAAQAAAAEAAVcQAAAAAAC1oZGxyAAAAAAAAAAB2aWRlAAAAAAAAAAAAAAAAVmlkZW9IYW5kbGVyAAAAATttaW5mAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAAD7c3RibAAAAJdzdHNkAAAAAAAAAAEAAACHYXZjMQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAACgAFoASAAAAEgAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABj//wAAADFhdmNDAWQACv/hABhnZAAKrNlCjfkhAAADAAEAAAMAAg8SJZYBAAZo6+JLIsAAAAAYc3R0cwAAAAAAAAABAAAAAQAAQAAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAEAAAABAAAAFHN0c3oAAAAAAAAC5QAAAAEAAAAUc3RjbwAAAAAAAAABAAAAMAAAAGJ1ZHRhAAAAWm1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAALWlsc3QAAAAlqXRvbwAAAB1kYXRhAAAAAQAAAABMYXZmNTguMTIuMTAw" type="video/mp4"</video>
8+
<script>
9+
const vid = document.getElementById('img');
10+
11+
console.log(vid);
12+
13+
vid.addEventListener('loadeddata', () => {
14+
const x = tf.browser.fromPixels(vid);
15+
16+
x.print();
17+
});
18+
19+
</script>

0 commit comments

Comments
 (0)