fe-notes icon indicating copy to clipboard operation
fe-notes copied to clipboard

关于图像处理的一些坑点

Open Inchill opened this issue 2 years ago • 0 comments

最近在做 OCR 识别相关的工作,图片都是通过前端上传,遇到了几个问题。

  1. 通过代码创建 input 标签来实现图片选择&拍照功能,在 iOS 手机上拍照确定后没有任何反应;
  2. 在 OCR 识别过程中出现不少识别失败的情况。

为了解决以上两个问题,做了相关的探究。

iOS 设备拍照确定后没有反应

先看下唤起图片上传的代码:

    const launchHtmlCamera = () => {
        const inputElement = document.createElement('input');
        inputElement.id = 'upload-image';
        inputElement.type = 'file';
        inputElement.accept = 'image/*';
        // 允许多选
        inputElement.multiple = true;
        inputElement.style.display = 'none';

        // 如果仅支持拍照,添加 capture 属性
        // if ('capture' in inputElement) {
        //     inputElement.capture = 'camera';
        // }

        inputElement.addEventListener('change', handleImageSelect);
        inputElement.click();
    };

代码比较简单,就是通过 js 创建 input 标签,设置系列属性,然后模拟点击事件触发图片上传。但是在测试中发现,iOS 手机拍照后没有触发回调函数,也就是 handleImageSelect 一直没有执行。

在 stackoverflow 上发现了解决办法,但是并没有找到原因。要在 iOS 手机上正常使用,必须得满足两个条件:

  1. input 标签必须被插入到 DOM 中;
  2. 必须使用 addEventListener 监听 change 事件,而不能使用 onchange。

所以只需要在模拟点击事件代码之前加上一行代码:

document.body.appendChild(inputElement); // 如果不插入 iOS 中事件监听器不会被触发

由于是采用的 react hooks 开发模式,还遇到另一个和闭包有关的问题。input 被插入到 DOM 后,在其上绑定了 handleImageSelect 函数,而这个函数里会设置新增的图片数据,因为在后续更新中访问这个变量一直是初次渲染时的值,导致添加一张图片后再次添加只发生替换而不新增图片。

为了解决这个办法,最佳做法是每次渲染时重新绑定事件回调函数或者是通过 useRef 保存对变量的引用。

第一种重新绑定事件回调函数,做法比较简单粗暴,就是在回调函数的最后将 input 标签从 DOM 中移除。

event.target.value = null;
const inputElement = document.getElementById('upload-image');
inputElement.removeEventListener('change', handleImageSelect);
document.body.removeChild(inputElement);

第二种方式,就是监听变量,然后在其变化后使用 ref 保存,在事件回调函数里使用 ref 来访问最新的变量。

const currentImgList = useRef([]);

    useEffect(() => {
        currentImgList.current = [...imgList];
    }, [imgList]);

const handleImageSelect = () => {
           const list = [...currentImgList.current, ...res.filter(Boolean)];
           setImgList(list);
}

另外需要调整的就是在每次唤起图片上传函数时,需要判断 input 标签是否已经插入到 DOM 中。

        let inputElement = document.getElementById('upload-image');
        if (inputElement) {
            inputElement.click();
            return;
        }

iOS 拍照 OCR 无法识别

在使用 iOS 手机拍照的时候,图片预览是竖直方向,但是 OCR 系统反馈识别失败。这是因为 OCR 在识别的过程中,会把图片默认当作旋转角度为 0 的图片来识别,我们预览看到的图片,其实是系统帮我们旋转了角度的,这个可以通过 EXIF 信息能看到。

Screenshot 2024-02-23 at 10 21 20

比如这张图片,我们在设备上看是正常的,但是系统帮我们逆时针旋转了 90 度,图片真实角度是顺时针旋转了 90 度的。而 OCR 系统识别过程中会按照区域进行识别,这就导致识别失败。比如说要求的图片右上角必须是一个二维码,但是由于真实的图片被旋转了,二维码出现在右下角了,OCR 框定的区域就找不到二维码了。

Screenshot 2024-02-23 at 10 35 02

手机拍照图片上传时经常遇到图片旋转的问题,需要设置 EXIF 中的 Orientation 参数。Orientation 存储的是手机的拍摄方向,获取 Orientation 信息可以借助一个开源工具库 exif-js。关于图像处理 Orientation 的相关信息,可以从这篇文章详细阅读:笔记:使用 JavaScript 读取 JPEG 文件 EXIF 信息中的 Orientation 值

比如我们要把真实的图片绘制出来,可以通过如下代码:

/**
 * 根据文件扩展信息还原图片数据
 * @param {*} file 文件
 * @returns blob
 */
export const getExif = (file) => {
    return new Promise((resolve) => {
        // 创建一个Image对象
        const img = new Image();
        // 读取图片数据
        const reader = new FileReader();

        reader.onload = (e) => {
            img.src = e.target.result;
            // 图片加载完成后进行旋转处理
            img.onload = function () {
                // 获取方向信息
                EXIF.getData(img, () => {
                    const orientation = EXIF.getTag(img, 'Orientation');
                    console.log('orientation', orientation);

                    // 创建一个Canvas
                    const canvas = document.createElement('canvas');
                    const context = canvas.getContext('2d');

                    // 根据方向信息旋转图片
                    switch (orientation) {
                        case 3:
                            canvas.width = img.height;
                            canvas.height = img.width;
                            context.rotate(Math.PI);
                            context.drawImage(img, -img.width, -img.height);
                            break;
                        case 6:
                            canvas.width = img.height;
                            canvas.height = img.width;
                            context.rotate(Math.PI / 2);
                            context.drawImage(img, 0, -img.height);
                            break;
                        case 8:
                            canvas.width = img.height;
                            canvas.height = img.width;
                            context.rotate(-Math.PI / 2);
                            context.drawImage(img, -img.width, 0);
                            break;
                        default:
                            canvas.width = img.width;
                            canvas.height = img.height;
                            context.drawImage(img, 0, 0);
                    }

                    // 将Canvas中的图像转为 blob
                    canvas.toBlob(resolve);
                });
            };
        };
        reader.readAsDataURL(file);
    });
};

当然,我们给 OCR 的图片,得把 Orientation 信息设置为 1,无论怎么翻转角度,我都当作是旋转角度为 0 来处理,这里通过 canvas 的方式重新绘制图片:

/**
 * 将图片的 orientation 重置为 1
 * @param {*} file 文件
 * @returns blob
 */
export const formatOrientation = (file) => {
    return new Promise((resolve) => {
        // 创建一个Image对象
        const img = new Image();
        // 读取图片数据
        const reader = new FileReader();

        reader.onload = (e) => {
            img.src = e.target.result;
            // 图片加载完成后进行旋转处理
            img.onload = function () {
                // 创建一个Canvas
                const canvas = document.createElement('canvas');
                const context = canvas.getContext('2d');

                // 设置Canvas的宽度和高度,确保它足够容纳整个图片
                canvas.width = img.width;
                canvas.height = img.height;

                // 不再根据方向信息旋转图片,直接绘制原始图片
                context.drawImage(img, 0, 0);

                // 将Canvas中的图像转为 blob
                canvas.toBlob(resolve, file?.type, 0.5);
            };
        };
        reader.readAsDataURL(file);
    });
};

这样处理过后,OCR 识别失败的例子就减少了很多,如果 OCR 还不能识别,就需要 OCR 系统把图片翻转 180 度再进行识别。

Inchill avatar Feb 22 '24 02:02 Inchill