由于项目实际应用中,使用的 qrcode-decoder,发现识别成功率过低,而且扫描件经常识别不到二维码,或者带有icon的二维码,识别率也惨不忍睹,后面引入了opencv-js-qrcode进行再次优化提升,进一步提升 vue,js,html,识别带二维码的发票前端支持pdf转图片 此文章的二维码识别成功率。
首先想到的是,截取发票左上角二维码位置,在进行放大,提高扫描率成功率,然后我就直接截取二维码区域上传进行扫描,发现成功率还是太低,或者干脆识别不到,于是打算用java后台处理,发现用 hutool包下面的二维码工具 成功率跟前端是一样的,前端识别到的后端也能识别到,识别不到的都识别不到...
但是我用手机是直接能扫出来的,或者微信的扫描能直接扫出来, 发现可能是扫描的方式有问题,在网上冲浪了一番,发现 知乎上JS识别照片或图片中的二维码 这个文章最靠谱:
github:https://github.com/leidenglai/opencv-js-qrcode
opencv-js-qrcode demo链接:https://leidenglai.github.io/opencv-js-qrcode/
经测试后发现功能确实强大然后我下载资源封装到了系统中:
opencvUtils.js
/**
* 扫描对象
* @constructor
*/
function OpencvUtils() {
this.loadScript = function (url) {
return new Promise((resolve, reject) => {
let script = document.createElement('script');
script.setAttribute('async', '');
script.setAttribute('type', 'text/javascript');
script.setAttribute('id', 'opencvjs');
script.addEventListener('load', async () => {
if (cv.getBuildInformation) {
resolve();
} else {
// WASM
if (cv instanceof Promise) {
cv = await cv;
resolve();
} else {
cv['onRuntimeInitialized'] = () => {
resolve();
};
}
}
});
script.addEventListener('error', () => {
reject();
});
script.src = url;
let node = document.getElementsByTagName('script')[0];
node.parentNode.insertBefore(script, node);
});
};
/**
* 请求二维码训练模型文件
* @param modelUrl
* @returns {Promise<Uint8Array>}
*/
this.fetchModelsData = async function (modelUrl) {
// 这里是放到public下面的文件
const response = await fetch(modelUrl, {
method: 'GET'
});
const data = await response.arrayBuffer();
return new Uint8Array(data);
};
/**
* 加载图片到canvas
* 发票的二维码基本都在左上角
* 为提高效率,只截取出图片二维码的左上角区域放入canvas
* @param {*} url
* @param {*} callBack
*/
this.loadImageToCanvas = function (url, callBack) {
let canvas = document.createElement('canvas');
canvas.id = 'openCVCanvasInput';
let ctx = canvas.getContext('2d');
let img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const { width, height } = img;
const isVertical = width < height;
const crossNum = isVertical ? 3 : 4;
const verticalNum = isVertical ? 4 : 3;
canvas.width = width / crossNum;
canvas.height = height / verticalNum;
ctx.drawImage(img, isVertical ? width * (2 / 3) : 0, 0, width, height, 0, 0, width, height);
// 加载完回调
if (callBack) {
callBack();
}
};
document.body.appendChild(canvas);
img.src = url;
};
/**
* canvas转图片
*/
this.imagedataToImage = function (imagedata) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = imagedata.width;
canvas.height = imagedata.height;
ctx.putImageData(imagedata, 0, 0);
return new Promise(resolve => {
const img = new Image();
img.src = canvas.toDataURL();
img.onload = () => {
resolve(img);
};
});
};
/**
* 拆分图片坐标
* @param {*} width 图片宽
* @param {*} height 图片高
*
* @returns 坐标数组 [x,y,width,height][]
*/
this.segmentationImageCoordinates = function (width, height) {
const isVertical = width < height;
const crossNum = isVertical ? 3 : 5;
const verticalNum = isVertical ? 5 : 3;
const blockWidth = width / crossNum;
const blockHeight = height / verticalNum;
const coordinates = [];
for (let y = 0; y < verticalNum; y++) {
for (let x = 0; x < crossNum; x++) {
const cx = x * blockWidth;
const cy = y * blockHeight;
coordinates.push([cx, cy, blockWidth, blockHeight]);
}
}
return coordinates;
};
}
// 临时扫描对象
let qrcode_detector = undefined;
// 临时工具类对象
let tempUtils = null;
/**
* 加载模型进行初始化
* @returns {Promise<void>}
*/
async function loadModels() {
if (qrcode_detector !== undefined) {
console.log('Model Existed');
} else {
// 读取public下面的扫描模型
const dp = await tempUtils.fetchModelsData('/static/models/detect.prototxt');
const dw = await tempUtils.fetchModelsData('/static/models/detect.caffemodel');
const sp = await tempUtils.fetchModelsData('/static/models/sr.prototxt');
const sw = await tempUtils.fetchModelsData('/static/models/sr.caffemodel');
// 给cv 创建临时文件夹存放模型数据
cv.FS_createDataFile('/', 'detect.prototxt', dp, true, false, false);
cv.FS_createDataFile('/', 'detect.caffemodel', dw, true, false, false);
cv.FS_createDataFile('/', 'sr.prototxt', sp, true, false, false);
cv.FS_createDataFile('/', 'sr.caffemodel', sw, true, false, false);
// 创建扫描方法
qrcode_detector = new cv.wechat_qrcode_WeChatQRCode('detect.prototxt', 'detect.caffemodel', 'sr.prototxt', 'sr.caffemodel');
console.log('OpenCV Model Created');
}
}
// 初始化
function initOpencv() {
// 创建临时对象
tempUtils = new OpencvUtils();
// 加载opencv.js到页面中
tempUtils
.loadScript('/static/opencv.js')
.then(() => {
// 加载模型
loadModels();
})
.catch(e => {
console.log('Failed to load ' + 'opencv.js', e);
});
}
// 初始化
initOpencv();
/**
* opencv 扫描二维码
* @param qrcodeUrl
* @param callBack
*/
export function qrcode_run(qrcodeUrl, callBack) {
console.time('OpenCV耗时');
// 创建画布
tempUtils.loadImageToCanvas(qrcodeUrl, () => {
let inputImage = cv.imread('openCVCanvasInput', cv.IMREAD_GRAYSCALE);
let points_vec = new cv.MatVector();
let res = qrcode_detector.detectAndDecode(inputImage, points_vec);
if (res.size() > 0) {
console.log('opencv识别结果:', res.get(0));
if (callBack) {
callBack(res?.get(0) || null);
}
/* 这一步是把截图出来的二维码放进画布显示,我不需要就没有打开
// document.querySelector('#qrcodeResult span').innerHTML = res.get(0);
let points = points_vec.get(0);
let x = points.floatAt(0);
let y = points.floatAt(1);
let width = points.floatAt(4) - points.floatAt(0);
let height = points.floatAt(5) - points.floatAt(1);
let rect = new cv.Rect(x, y, width, height);
dst = inputImage.roi(rect);
cv.imshow('qrcodeCanvasOutput', dst);*/
}
// 删除画布对象
document.getElementById('openCVCanvasInput').remove(); // 下载完成移除元素
console.timeEnd('OpenCV耗时');
});
}
资源文件放在public目录下,方便请求访问二进制数据:
这个资源需要你去git上自己下载资源:https://github.com/leidenglai/opencv-js-qrcode
使用方法:
import { qrcode_run } from '@/utils/opencv/opencvUtils';
引入后会直接触发初始化方法,因为要加载资源差不多5M大小,所以建议直接引入加载就行了,防止用的时候等待;
// 初始化opencv数据
initOpencv();
生命周期流程:
1.将opencv.js加载到页面
2.加载完成后调用模型数据加载
3.模型文件获取成功后将资源文件转换为 Uint8Array 加载到引擎中
4.初始化完成
5.调用扫描
6.创建画布 加载图片到canvas
7.图片加载完成后进行识别
8.识别后删除画布对象
先调用qrcode-decoder进行扫描(详情见上篇文章),如果扫描不到数据在调用opencv进行识别:
注意:使用opencv截图的时候还是有部分问题的,可以先去 opencv-js-qrcode的demo 上测试,在考虑要不要在项目中使用。
像是这种,两种方式是都识别不到。