Skip to content

关于图像处理的一些坑点 #56

@Inchill

Description

@Inchill

最近在做 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 度再进行识别。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions