【问题标题】:Canvas getImageData returning incorrect data on certain mobile devicesCanvas getImageData 在某些移动设备上返回不正确的数据
【发布时间】:2018-01-22 15:27:41
【问题描述】:

我正在开发一个基于视频帧的具有一些特殊功能的画布视频播放器。为了克服视频 HTML5 标记中不可靠的时序,我们使用的视频在每个帧中嵌入了一个条形码,指示当前帧号。使用画布 getImageData 方法,我可以抓取像素并读取条形码以获取帧号。这很好用,我有一个JSFiddle 证明它有效(我无法在这个小提琴中绕过 CORS 将视频提供到画布上,所以要看到它工作你必须在本地下载示例视频然后上传它通过按钮。不理想,但它有效)。

在某些移动设备(目前只有 Android)上,这种逻辑会被打破。 getImageData 返回不正确的值。

它在我的三星 Galaxy S5 v6.0.1 上正常工作,但在运行 android v7.1.2 的 Google Pixel 上失败。我会尝试收集更多关于失败的设备/操作系统版本的数据。

例如,在桌面上播放时,getImageData 的第一次迭代返回:

Uint8ClampedArray(64) [3, 2, 3, 255, 255, 255, 255, 255, 246, 245, 247, 255, 243, 242, 243, 255, 241, 239, 241, 255, 242, 240, 242, 255, 242, 240, 242, 255, 242, 240, 242, 255, 242, 240, 242, 255, 242, 240, 242, 255, 242, 240, 242, 255, 242, 240, 242, 255, 242, 240, 242, 255, 242, 240, 242, 255, 242, 240, 242, 255, 242, 240, 242, 255]

正确计算为帧号 1。

然而在银河系上,第一次迭代返回:

Uint8ClampedArray(64) [255, 242, 217, 255, 255, 234, 209, 255, 41, 1, 1, 255, 254, 235, 210, 255, 255, 234, 209, 255, 50, 4, 0, 255, 254, 240, 215, 255, 255, 248, 224, 255, 255, 249, 225, 255, 255, 251, 232, 255, 255, 252, 233, 255, 255, 252, 233, 255, 255, 253, 234, 255, 255, 255, 237, 255, 255, 255, 237, 255, 28, 1, 1, 255] 

我读到某些设备可能会进行额外的平滑处理,所以我一直在尝试通过以下方式在上下文中禁用它:

this.ctx.mozImageSmoothingEnabled = false;
this.ctx.webkitImageSmoothingEnabled = false;
this.ctx.msImageSmoothingEnabled = false;
this.ctx.imageSmoothingEnabled = false; 

但这没有帮助。

这是 JSFiddle 中使用的代码。

var frameNumberDiv = document.getElementById('frameNumber');
function load() {
    var canvas = document.getElementById('canvas');
    var ctx = canvas.getContext('2d');
    var video = document.getElementById('video');
    canvas.width = 568;
    canvas.height = 640;
    video.addEventListener('play', function() {
        var that = this; //cache
        (function loop() {
            if (!that.paused && !that.ended) {
                ctx.drawImage(that, 0, 0);
                var pixels = ctx.getImageData(0, 320 - 1, 16, 1).data;
                getFrameNumber(pixels);
                setTimeout(loop, 1000 / 30); // drawing at 30fps
            }
        })();
    }, 0);
}

function getFrameNumber(pixels) {

    let j = 0;
    let thisFrameNumber = 0;
    let str = "Pixels: ";
    for (let i = 0; i < 16; i++) {
        str += pixels[j] + " ";
        thisFrameNumber += getBinary(pixels[j], i);
        j += 4;
    }
    document.getElementById('frameNumber').innerHTML = "FrameNumber: " + thisFrameNumber;
}

function getBinary(pixel, binaryPlace) {
    const binary = [1, 2, 4, 8, 16, 32, 64, 128, 256,
        512, 1024, 2048, 4096, 8192, 16384, 32768
    ];
    if (pixel > 128) return 0;
    if (pixel < 128 && binary[binaryPlace]) {
        return binary[binaryPlace]
    } else {
        return 0;
    }
}

(function localFileVideoPlayer() {
    'use strict';
    var URL = window.URL || window.webkitURL;
    var displayMessage = function(message, isError) {
        var element = document.querySelector('#message');
        element.innerHTML = message;
        element.className = isError ? 'error' : 'info';
    }
    var playSelectedFile = function(event) {
            console.log("Playing");
        var file = this.files[0];
        var type = file.type;
        var videoNode = document.querySelector('video');
        var canPlay = videoNode.canPlayType(type);
        if (canPlay === '') canPlay = 'no';
        var message = 'Can play type "' + type + '": ' + canPlay;
        var isError = canPlay === 'no';
        displayMessage(message, isError);

        if (isError) {
            return;
        }

        var fileURL = URL.createObjectURL(file);
        videoNode.src = fileURL;
        load();
    }
    var inputNode = document.querySelector('input')
    inputNode.addEventListener('change', playSelectedFile, false)
})();

编辑

  • 适用于运行 Android v6.0 的 Nexus 6P
  • 适用于运行 Android v 5.0.2 的三星 6 (Samsung SM G920A)
  • 不能在运行 Android v7.0 的三星 Galaxy S7 (SAMSUNG-SM-G935A) 上工作

这可能是 Android 7 问题吗?

编辑 2

回答cmets中的一个问题:

videoNode.videoHeight 和 videoWidth 在谷歌像素上都是 0,但在桌面上是一样的。在我遇到的两种不工作的设备中,每一帧的图像都是完全绘制的。我将附上谷歌像素的屏幕截图。 暂停时,它始终读取相同的数字。 换句话说,它不会跳来跳去,因此它正在读取的内容确实在视频的帧上。

编辑 3:发现

我相信我已经做出了我应该早点看到的相关发现/实现。

查看损坏设备上 getImageData 的输出时,我正在逐行执行。我没有(并且应该)注意到的是,视频元素在达到我的断点/调试器语句后会稍微继续。到执行 getImageData 方法时,视频已经移过了下一帧。因此,扫描的条形码实际上比预期的要晚得多。

我添加了一些控制台日志语句,让它自然运行。查看输出,我可以看到一个更容易识别的模式。

这是关于 google 像素的前几个读数:

Uint8ClampedArray(64) [255, 255, 255, 255, 246, 246, 246, 255, 243, 243, 243, 255, 240, 240, 241, 255, 241, 241, 242, 255, 241, 241, 242, 255, 241, 241, 242, 255, 241, 241, 242, 255, 241, 241, 242, 255, 241, 241, 242, 255, 241, 241, 242, 255, 241, 241, 242, 255, 241, 241, 242, 255, 241, 241, 242, 255, 241, 241, 242, 255, 2, 2, 2, 255]

Uint8ClampedArray(64) [5, 5, 5, 255, 255, 255, 255, 255, 251, 251, 251, 255, 247, 247, 248, 255, 245, 245, 245, 255, 245, 245, 245, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 246, 246, 246, 255, 6, 3, 2, 255]

Uint8ClampedArray(64) [235, 231, 230, 255, 17, 12, 12, 255, 252, 247, 247, 255, 255, 255, 255, 255, 255, 254, 254, 255, 255, 253, 253, 255, 255, 252, 251, 255, 255, 253, 253, 255, 255, 253, 253, 255, 255, 253, 253, 255, 255, 253, 253, 255, 255, 253, 253, 255, 255, 253, 253, 255, 255, 253, 253, 255, 255, 253, 253, 255, 7, 3, 1, 255]

Uint8ClampedArray(64) [26, 15, 14, 255, 4, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 10, 0, 0, 255]

您可能会注意到,结果似乎是正确的,但是它们向左移动了一个像素。

我稍微修改了JSFiddle 以将读取的getImageData 移动一个像素,它给出的响应与在像素上完全相同。

var pixels = ctx.getImageData(1, 320 - 1, 16, 1).data;

做-1似乎没有效果。

因此,出于某种原因,这些设备要么将整个纹理移动了一个像素,要么 getImageData 方法出现问题。

编辑 4

作为一个实验,我重新配置了我的代码以使用 webGL 纹理。台式机/移动设备上的行为相同。这允许我使用 gl.readPixels 将 -1 用作 x 目标。我希望通过跳过使用画布,整个图像将存储在内存中,我可以访问我需要的像素数据....没用,但这是它产生的数据,表明它也使用纯 webGL 进行了移动.

Uint8Array(64) [0, 0, 0, 0, 255, 248, 248, 255, 25, 18, 18, 255, 254, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]

Uint8Array(64) [0, 0, 0, 0, 255, 254, 247, 255, 255, 244, 236, 255, 48, 18, 10, 255, 254, 246, 238, 255, 255, 247, 239, 255, 255, 247, 239, 255, 255, 248, 241, 255, 255, 251, 243, 255, 255, 251, 243, 255, 255, 251, 243, 255, 255, 251, 243, 255, 255, 251, 243, 255, 255, 251, 243, 255, 255, 250, 240, 255, 255, 250, 240, 255]

Uint8Array(64) [0, 0, 0, 0, 31, 0, 0, 255, 254, 243, 230, 255, 43, 6, 1, 255, 254, 243, 230, 255, 255, 244, 231, 255, 255, 244, 231, 255, 255, 244, 231, 255, 255, 244, 231, 255, 255, 244, 231, 255, 255, 244, 231, 255, 255, 244, 231, 255, 255, 244, 231, 255, 255, 244, 231, 255, 255, 244, 229, 255, 255, 244, 229, 255]

使用:

gl.readPixels(-1, height, 16, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

编辑 5

好的,我保证会详细了解哪些设备出现故障/未出现故障。我使用稍作修改的JSFiddle 完成了一些在线 QA 测试。我对其稍作修改,以使其更适合普通大众使用。

不幸的是,反应相当复杂。我希望它会被隔离到 Android 7,但似乎并非如此。

我的谷歌驱动器上有一个CSV,上面有这个测试的结果。并不是说这些测试是 100% 可靠的,但它似乎只是一些随机设备......

【问题讨论】:

  • 此设备上@9​​87654335@ 和videoNode.videoWidth 的值是多少?如果您不对画布的宽度和高度进行硬编码,而是使用这些值以及drawImage(vid, 0,0,vid.videoWidth,vid.videoHeight),会发生什么情况。所有的图像都画在画布的所有表面上吗?是否仅在播放视频时发生(即在视频暂停时是否有效?)
  • 嘿@Kaiido 我已经进行了编辑以解决您的问题。谢谢!
  • 对于 videoHeight/videoWidth 需要等待 onloadedmetadata 事件。它不能是 0。
  • 你写的条码怎么样?
  • @clabe45 很抱歉,我实际上并不知道。它们是由另一个团队制作的。我的猜测是 After Effects。

标签: javascript html canvas


【解决方案1】:

我也遇到过同样的问题,只是无法解决。在我的情况下,我终于有了答案,这听起来 99% 也像你的问题。

这是像素密度!!

您提到的所有设备的像素密度都不同,并且可能具有 1 dpr(设备像素比)的设备工作正常,而其他设备则没有。

所以在我的例子中使用 p5js 我将像素密度设置为 1,它就像一个魅力;

pixelDensity(1);

所以将它设置为 1 dpr,你很可能很好去!

我希望这可以帮助一些人,因为我在这个问题上花了很长时间。

【讨论】:

  • 您还可以通过以下方式访问像素密度:window.devicePixelRatio
【解决方案2】:

您的代码很好,所以这里的问题在于某些机器人如何呈现您的条形码。您的条形码的实现太小(16x1 像素),而且它是缩进的。

设备在外部像素上产生的任何反锯齿都会弄乱您的条形码并给您不正确的结果,因此,在处理视频渲染时,您绝对不想在一个像素安全区域上工作。

我的建议是将条形码重做更大的尺寸 - 假设高度为 18 像素 - 仅使用黑白(无灰色),将其置于视频中心,将线条的其余部分涂成绿色:(在此例如它是“1”的条形码)

然后制作一个完整的 320x16 的 GetImageData 并去掉所有 RGB 为 0x255x0(和近似值)的东西,你就有了可以用来获取 FrameNumber 的条形码。

我知道您可能想要一个不需要重做视频的答案,但在这种情况下,问题出在源头。

【讨论】:

    猜你喜欢
    • 2012-01-17
    • 1970-01-01
    • 2017-11-18
    • 2017-05-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-10-18
    • 1970-01-01
    相关资源
    最近更新 更多