mirror of
https://github.com/cnwhy/QRCode-decode.git
synced 2026-08-13 04:51:41 +08:00
Compare commits
4
Commits
v0.0.2
...
f799ae2b40
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f799ae2b40 | ||
|
|
c6f33bf5c9 | ||
|
|
bfea903e80 | ||
|
|
14edfc7fbb |
+26
-23
@@ -16,7 +16,7 @@
|
|||||||
<div></div>
|
<div></div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
<script type="text/javascript" src="../dist/qr-decode.js"></script>
|
<script type="text/javascript" src="../dist/qr-decode.umd.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.getElementById('file').onchange = function (event) {
|
document.getElementById('file').onchange = function (event) {
|
||||||
var el = event.target;
|
var el = event.target;
|
||||||
@@ -48,48 +48,51 @@
|
|||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var video =document.getElementById('video');
|
var video = document.getElementById('video');
|
||||||
var videoBut =document.getElementById('videoBut');
|
var videoBut = document.getElementById('videoBut');
|
||||||
var xc;
|
var xc;
|
||||||
videoBut.onclick = videoEnable;
|
videoBut.onclick = videoEnable;
|
||||||
|
|
||||||
function videoEnable() {
|
function videoEnable() {
|
||||||
var URL = window.URL || window.webkitURL;
|
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
||||||
navigator.getUserMedia({
|
navigator.mediaDevices.getUserMedia({
|
||||||
video: true
|
video: true
|
||||||
}, function (stream) {
|
}).then(function (stream) {
|
||||||
video.src = URL.createObjectURL(stream);// 将获取到的视频流对象转换为地址
|
video.srcObject = stream; // 用 srcObject 替代已废弃的 URL.createObjectURL(stream)
|
||||||
video.oncanplay = function(){
|
video.oncanplay = function () {
|
||||||
videoPlay();
|
videoPlay();
|
||||||
video.width = video.videoWidth;
|
video.width = video.videoWidth;
|
||||||
video.height = video.videoHeight;
|
video.height = video.videoHeight;
|
||||||
}
|
}
|
||||||
}, function (error) {
|
}).catch(function (error) {
|
||||||
alert(error.name || error);
|
alert(error.name || error);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
alert('当前浏览器不支持摄像头访问');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function videoStop(){
|
function videoStop() {
|
||||||
clearInterval(xc);
|
clearInterval(xc);
|
||||||
video.pause();
|
video.pause();
|
||||||
videoBut.innerText = '启动';
|
videoBut.innerText = '启动';
|
||||||
videoBut.onclick = videoPlay;
|
videoBut.onclick = videoPlay;
|
||||||
}
|
}
|
||||||
function videoPlay(){
|
function videoPlay() {
|
||||||
video.play();
|
video.play();
|
||||||
videoBut.innerText = '停止';
|
videoBut.innerText = '停止';
|
||||||
xc = setInterval(function(){
|
xc = setInterval(function () {
|
||||||
try{
|
try {
|
||||||
var txt = qrDecode.decodeByDom(video);
|
var txt = qrDecode.decodeByDom(video);
|
||||||
var msg = document.createElement("div")
|
var msg = document.createElement("div")
|
||||||
msg.innerHTML = "识别到二维码: " + txt;
|
msg.innerHTML = "识别到二维码: " + txt;
|
||||||
document.body.appendChild(msg);
|
document.body.appendChild(msg);
|
||||||
videoStop();
|
videoStop();
|
||||||
}catch(err){
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
},300)
|
}, 300)
|
||||||
videoBut.onclick = videoStop;
|
videoBut.onclick = videoStop;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Vendored
+2787
File diff suppressed because one or more lines are too long
Vendored
-7
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
+528
-773
File diff suppressed because one or more lines are too long
Vendored
+7
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
+12420
File diff suppressed because it is too large
Load Diff
+6
-5
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "qr-decode",
|
"name": "qr-decode",
|
||||||
"version": "0.0.2",
|
"version": "0.0.6",
|
||||||
"description": "QRCode parser/decode",
|
"description": "QRCode parser/decode",
|
||||||
"main": "src/QRCodeDecode.js",
|
"main": "src/QRDecode.js",
|
||||||
"files": [
|
"files": [
|
||||||
"README.md",
|
"README.md",
|
||||||
"browser.js",
|
"browser.js",
|
||||||
@@ -12,8 +12,9 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node test/node.js",
|
"test": "node test/node.js",
|
||||||
|
"test:regression": "node test/all_regression.js",
|
||||||
"demo": "parcel ./demo/index.html",
|
"demo": "parcel ./demo/index.html",
|
||||||
"build": "bili browser.js --format umd,umd-min --banner"
|
"build": "bili browser.js --format umd,umd-min,es --module-name qrDecode --file-name qr-decode.[format][min][ext] --banner"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -34,7 +35,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/cnwhy/QRCode-decode#readme",
|
"homepage": "https://github.com/cnwhy/QRCode-decode#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bili": "^3.1.2",
|
"bili": "^5.0.5",
|
||||||
"parcel": "^1.9.7"
|
"parcel": "^1.9.7"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -42,4 +43,4 @@
|
|||||||
"image-type": "^3.0.0",
|
"image-type": "^3.0.0",
|
||||||
"pako": "^1.0.6"
|
"pako": "^1.0.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
var fs = require('fs')
|
var fs = require('fs')
|
||||||
var imgType = require('image-type')
|
var imgType = require('image-type')
|
||||||
var imgDecode = require('./src/imageDecode')
|
var imgDecode = require('./src/imageDecode')
|
||||||
var qrDecode = require('./src/QRDecode')
|
var qrDecode = require('./src/QRDecode');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过Buffer识别二维码
|
* 通过Buffer识别二维码
|
||||||
|
|||||||
+307
-14
@@ -3,20 +3,28 @@ var Decoder = require('./lib/Decoder');
|
|||||||
var debug = false;
|
var debug = false;
|
||||||
|
|
||||||
|
|
||||||
// 获取指定位置的灰度
|
/**
|
||||||
|
* 返回获取指定位置的灰度的函数
|
||||||
|
* @param {ImageData} data
|
||||||
|
* @param {Object} base 一个有width,height信息
|
||||||
|
*/
|
||||||
var Pixel = function (data,base) {
|
var Pixel = function (data,base) {
|
||||||
return function(x,y){
|
return function(x,y){
|
||||||
if (base.width < x || base.height < y) {
|
if (base.width < x || base.height < y) {
|
||||||
throw "point error";
|
throw "point error";
|
||||||
}
|
}
|
||||||
var point = (x * 4) + (y * base.width * 4)
|
var point = (x * 4) + (y * base.width * 4)
|
||||||
|
// 透明/半透明像素按白色背景处理,避免透明区域被误判为黑色模块
|
||||||
|
if (data[point + 3] < 128) {
|
||||||
|
return 255;
|
||||||
|
}
|
||||||
return (data[point] * 33.33 + data[point + 1] * 33.33 + data[point + 2] * 33.33) / 100;
|
return (data[point] * 33.33 + data[point + 1] * 33.33 + data[point + 2] * 33.33) / 100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var binarize = function(data,base,th){
|
var binarize = function(data,base,th){
|
||||||
var ret = new Array(base.width * base.height);
|
var ret = new Array(base.width * base.height);
|
||||||
var getPixel = Pixel(data,base);
|
var getPixel = Pixel(data,base); //
|
||||||
for (var y = 0; y < base.height; y++) {
|
for (var y = 0; y < base.height; y++) {
|
||||||
for (var x = 0; x < base.width; x++) {
|
for (var x = 0; x < base.width; x++) {
|
||||||
var gray = getPixel(x, y);
|
var gray = getPixel(x, y);
|
||||||
@@ -106,20 +114,274 @@ var grayScaleToBitmap = function (image,base) {
|
|||||||
return bitmap;
|
return bitmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
function decode(imageDate,debugfn){
|
// 默认二值化阈值
|
||||||
var base = {
|
var DEFAULT_THRESHOLD = 153;
|
||||||
|
|
||||||
|
// 图像预处理策略:输入 ImageData,输出 {data, width, height, threshold?}
|
||||||
|
// threshold 可以是单值或数组(多阈值依次尝试)
|
||||||
|
// 后续优化(自适应二值化、放大等)只需在此数组追加策略即可
|
||||||
|
var preprocessStrategies = [
|
||||||
|
null, // 0. 原图原阈值
|
||||||
|
invertImage, // 1. 颜色反转(适配深色背景的反色二维码)
|
||||||
|
otsuStrategy, // 2. Otsu 自动阈值(适配整体偏亮/偏暗的图)
|
||||||
|
invertedOtsuStrategy, // 3. 反转 + Otsu(适配整体偏暗的反色二维码)
|
||||||
|
multiThresholdStrategy // 4. 多阈值扫描(兜底 Otsu 盲区:深色装饰背景等多峰分布美化二维码)
|
||||||
|
];
|
||||||
|
|
||||||
|
// 多阈值扫描集合:仅在前 4 个策略全部失败后触发
|
||||||
|
// 覆盖 Otsu 无法处理的多峰分布图(如“深色背景+灰模块+白间隔”三峰,Otsu 会选错谷),
|
||||||
|
// 以及带过渡带的低对比度图(Otsu 分割点偏低,需更高阈值才能获得清晰模块)
|
||||||
|
// 步长 15 覆盖全区间:o3.png 有效区间 125~150,o2.png 有效区间 180~240
|
||||||
|
var multiThresholdValues = [60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240];
|
||||||
|
|
||||||
|
// 多阈值策略:复用原图数据,对多阈值集合依次尝试
|
||||||
|
function multiThresholdStrategy(imageDate) {
|
||||||
|
return {
|
||||||
|
data: imageDate.data,
|
||||||
width: imageDate.width,
|
width: imageDate.width,
|
||||||
height: imageDate.height,
|
height: imageDate.height,
|
||||||
debugfn: debugfn
|
threshold: multiThresholdValues
|
||||||
}
|
};
|
||||||
return process(imageDate.data,base)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var process = function (data,base) {
|
// 颜色反转:RGB 取反,alpha 保持不变
|
||||||
|
function invertImage(imageDate) {
|
||||||
|
var data = new Uint8ClampedArray(imageDate.data);
|
||||||
|
for (var i = 0; i < data.length; i += 4) {
|
||||||
|
data[i] = 255 - data[i];
|
||||||
|
data[i + 1] = 255 - data[i + 1];
|
||||||
|
data[i + 2] = 255 - data[i + 2];
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
data: data,
|
||||||
|
width: imageDate.width,
|
||||||
|
height: imageDate.height
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otsu 大津法:穷举 0-255 阈值,求类间方差最大者作为最优全局阈值
|
||||||
|
function otsuThreshold(imageDate) {
|
||||||
|
var data = imageDate.data;
|
||||||
|
|
||||||
|
// 1. 灰度直方图(一次扫描,跳过透明/半透明像素)
|
||||||
|
// 透明区域在二值化时按白处理(见 Pixel),不影响检测;
|
||||||
|
// 但统计时计入会拉偏阈值,故只统计真实可见像素
|
||||||
|
var hist = new Array(256);
|
||||||
|
for (var i = 0; i < 256; i++) hist[i] = 0;
|
||||||
|
var total = 0;
|
||||||
|
for (var i = 0; i < data.length; i += 4) {
|
||||||
|
if (data[i + 3] < 128) continue;
|
||||||
|
var gray = (data[i] * 33.33 + data[i + 1] * 33.33 + data[i + 2] * 33.33) / 100 | 0;
|
||||||
|
hist[gray]++;
|
||||||
|
total++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 遍历阈值,找类间方差最大的
|
||||||
|
var sum = 0; // 全局灰度加权和
|
||||||
|
for (var i = 0; i < 256; i++) sum += i * hist[i];
|
||||||
|
var sumB = 0; // 背景类灰度加权和
|
||||||
|
var wB = 0; // 背景类像素数
|
||||||
|
var maxVar = -1;
|
||||||
|
var bestT = DEFAULT_THRESHOLD;
|
||||||
|
for (var t = 0; t < 256; t++) {
|
||||||
|
wB += hist[t];
|
||||||
|
if (wB === 0) continue;
|
||||||
|
var wF = total - wB;
|
||||||
|
if (wF === 0) break;
|
||||||
|
sumB += t * hist[t];
|
||||||
|
var mB = sumB / wB; // 背景类均值
|
||||||
|
var mF = (sum - sumB) / wF; // 前景类均值
|
||||||
|
var between = wB * wF * (mB - mF) * (mB - mF); // 类间方差
|
||||||
|
// 用 >= 在方差平坦时取最亮侧;阈值取两类均值中点,
|
||||||
|
// 避免双离散值图(类间方差平台)时阈值卡在类边界导致模块误判为白
|
||||||
|
if (between >= maxVar) {
|
||||||
|
maxVar = between;
|
||||||
|
bestT = Math.floor((mB + mF) / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otsu 策略:复用原图数据,仅提供自动计算的阈值(由 process 的 threshold 参数消费)
|
||||||
|
function otsuStrategy(imageDate) {
|
||||||
|
return {
|
||||||
|
data: imageDate.data,
|
||||||
|
width: imageDate.width,
|
||||||
|
height: imageDate.height,
|
||||||
|
threshold: otsuThreshold(imageDate)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 反转 + Otsu 策略:先反转,再对反转后的图计算 Otsu 阈值
|
||||||
|
// 覆盖“深色不够深、浅色不够浅”的偏暗反色二维码(固定阈值反转覆盖不了)
|
||||||
|
function invertedOtsuStrategy(imageDate) {
|
||||||
|
var inv = invertImage(imageDate);
|
||||||
|
return {
|
||||||
|
data: inv.data,
|
||||||
|
width: inv.width,
|
||||||
|
height: inv.height,
|
||||||
|
threshold: otsuThreshold(inv)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function decode(imageDate, debugfn) {
|
||||||
|
var lastErr;
|
||||||
|
for (var i = 0; i < preprocessStrategies.length; i++) {
|
||||||
|
var img = preprocessStrategies[i] ? preprocessStrategies[i](imageDate) : imageDate;
|
||||||
|
var base = {
|
||||||
|
width: img.width,
|
||||||
|
height: img.height,
|
||||||
|
debugfn: debugfn
|
||||||
|
};
|
||||||
|
// 支持多阈值:threshold 为数组时依次尝试,任一成功即返回
|
||||||
|
var thresholds = img.threshold;
|
||||||
|
if (!(thresholds instanceof Array)) {
|
||||||
|
thresholds = [thresholds];
|
||||||
|
}
|
||||||
|
for (var j = 0; j < thresholds.length; j++) {
|
||||||
|
try {
|
||||||
|
return process(img.data, base, thresholds[j]);
|
||||||
|
} catch (e) {
|
||||||
|
lastErr = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastErr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ECI 声明中常见的字符集(QR 规范:ECI 编号 -> 字符集)
|
||||||
|
var ECI_CHARSET = {
|
||||||
|
3: 'iso-8859-1',
|
||||||
|
20: 'shift_jis',
|
||||||
|
25: 'utf-16be',
|
||||||
|
26: 'utf-8',
|
||||||
|
27: 'us-ascii'
|
||||||
|
};
|
||||||
|
|
||||||
|
// UTF-8 字节数组转字符串
|
||||||
|
var decodeUTF8 = function (bytes) {
|
||||||
|
if (typeof TextDecoder !== 'undefined') {
|
||||||
|
return new TextDecoder('utf-8').decode(new Uint8Array(bytes));
|
||||||
|
}
|
||||||
|
var str = "";
|
||||||
|
var i = 0;
|
||||||
|
while (i < bytes.length) {
|
||||||
|
var code = bytes[i++] & 0xFF;
|
||||||
|
if (code < 0x80) {
|
||||||
|
str += String.fromCharCode(code);
|
||||||
|
}
|
||||||
|
else if (code < 0xE0) {
|
||||||
|
str += String.fromCharCode(((code & 0x1F) << 6) | (bytes[i++] & 0x3F));
|
||||||
|
}
|
||||||
|
else if (code < 0xF0) {
|
||||||
|
str += String.fromCharCode(((code & 0x0F) << 12) | ((bytes[i++] & 0x3F) << 6) | (bytes[i++] & 0x3F));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var cp = ((code & 0x07) << 18) | ((bytes[i++] & 0x3F) << 12) | ((bytes[i++] & 0x3F) << 6) | (bytes[i++] & 0x3F);
|
||||||
|
cp -= 0x10000;
|
||||||
|
str += String.fromCharCode((cp >> 10) + 0xD800, (cp & 0x3FF) + 0xDC00);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTF-16 字节数组转字符串(isLE=false 时先把 BE 转成 LE 统一处理)
|
||||||
|
var decodeUTF16 = function (bytes, isLE) {
|
||||||
|
if (!isLE) {
|
||||||
|
// UTF-16BE -> 交换字节序
|
||||||
|
for (var i = 0; i + 1 < bytes.length; i += 2) {
|
||||||
|
var t = bytes[i];
|
||||||
|
bytes[i] = bytes[i + 1];
|
||||||
|
bytes[i + 1] = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof TextDecoder !== 'undefined') {
|
||||||
|
return new TextDecoder('utf-16le').decode(new Uint8Array(bytes));
|
||||||
|
}
|
||||||
|
// 兜底:手写 UTF-16LE 解码(含代理对)
|
||||||
|
var str = "";
|
||||||
|
for (var i = 0; i + 1 < bytes.length; i += 2) {
|
||||||
|
var code = bytes[i] | (bytes[i + 1] << 8);
|
||||||
|
if (code >= 0xD800 && code <= 0xDBFF && i + 3 < bytes.length) {
|
||||||
|
var low = bytes[i + 2] | (bytes[i + 3] << 8);
|
||||||
|
if (low >= 0xDC00 && low <= 0xDFFF) {
|
||||||
|
str += String.fromCharCode(code, low);
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
str += String.fromCharCode(code);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8bit 字节段解码:按 ECI 声明 -> BOM -> 启发式检测 -> UTF-8 的顺序判断编码方式
|
||||||
|
var bytesToString = function (bytes, eci) {
|
||||||
|
var len = bytes.length;
|
||||||
|
if (len == 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 优先按 ECI 声明的字符集解码
|
||||||
|
if (eci && ECI_CHARSET[eci]) {
|
||||||
|
var charset = ECI_CHARSET[eci];
|
||||||
|
if (charset == 'utf-16be') {
|
||||||
|
return decodeUTF16(bytes, false);
|
||||||
|
}
|
||||||
|
if (charset == 'utf-8') {
|
||||||
|
return decodeUTF8(bytes);
|
||||||
|
}
|
||||||
|
if (typeof TextDecoder !== 'undefined') {
|
||||||
|
try {
|
||||||
|
return new TextDecoder(charset).decode(new Uint8Array(bytes));
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// 环境不支持该字符集时继续走下面的检测
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. BOM 检测
|
||||||
|
if (len >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE) {
|
||||||
|
return decodeUTF16(bytes.slice(2), true); // UTF-16LE
|
||||||
|
}
|
||||||
|
if (len >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF) {
|
||||||
|
return decodeUTF16(bytes.slice(2), false); // UTF-16BE
|
||||||
|
}
|
||||||
|
if (len >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) {
|
||||||
|
return decodeUTF8(bytes.slice(3)); // UTF-8 with BOM
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 无 BOM 时启发式检测 UTF-16(偶数长度且奇/偶位存在成片 0x00 字节)
|
||||||
|
if (len >= 4 && len % 2 == 0) {
|
||||||
|
var evenZeros = 0;
|
||||||
|
var oddZeros = 0;
|
||||||
|
for (var i = 0; i < len; i += 2) {
|
||||||
|
if (bytes[i] == 0) {
|
||||||
|
evenZeros++;
|
||||||
|
}
|
||||||
|
if (bytes[i + 1] == 0) {
|
||||||
|
oddZeros++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var half = len / 2;
|
||||||
|
if (oddZeros > half * 0.7) {
|
||||||
|
return decodeUTF16(bytes, true); // 奇数位大量 0x00 -> UTF-16LE
|
||||||
|
}
|
||||||
|
if (evenZeros > half * 0.7) {
|
||||||
|
return decodeUTF16(bytes, false); // 偶数位大量 0x00 -> UTF-16BE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 兜底默认 UTF-8
|
||||||
|
return decodeUTF8(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
var process = function (data,base,threshold) {
|
||||||
// var start = new Date().getTime();
|
// var start = new Date().getTime();
|
||||||
// var image = grayScaleToBitmap(grayscale(),base);
|
// var image = grayScaleToBitmap(grayscale(),base);
|
||||||
// var image = binarize(128);
|
// var image = binarize(128);
|
||||||
var image = binarize(data,base,153); //转为位图;
|
var image = binarize(data,base,threshold || DEFAULT_THRESHOLD); //转为位图;
|
||||||
|
|
||||||
debug && base.debugfn && base.debugfn(image,base.width);
|
debug && base.debugfn && base.debugfn(image,base.width);
|
||||||
|
|
||||||
@@ -139,12 +401,43 @@ var process = function (data,base) {
|
|||||||
|
|
||||||
// 解析QR矩阵
|
// 解析QR矩阵
|
||||||
var reader = Decoder.decode(qRCodeMatrix.bits);
|
var reader = Decoder.decode(qRCodeMatrix.bits);
|
||||||
// console.log(reader);
|
|
||||||
var data = reader.DataByte;
|
// 先读取编码方式(mode indicator),按编码方式将数据直接转成字符串
|
||||||
|
var MODE_NUMBER = 1;
|
||||||
|
var MODE_ROMAN_AND_NUMBER = 2;
|
||||||
|
var MODE_8BIT_BYTE = 4;
|
||||||
|
var MODE_ECI = 7;
|
||||||
|
var MODE_KANJI = 8;
|
||||||
|
|
||||||
var str = "";
|
var str = "";
|
||||||
for (var i = 0; i < data.length; i++) {
|
var eci = 0; // ECI 声明的字符集编号,作用于后续字节段
|
||||||
for (var j = 0; j < data[i].length; j++)
|
while (true) {
|
||||||
str += String.fromCharCode(data[i][j]);
|
var mode = reader.NextMode();
|
||||||
|
if (mode == 0)
|
||||||
|
break;
|
||||||
|
if (mode == MODE_ECI) {
|
||||||
|
eci = reader.parseECIValue();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var dataLength = reader.getDataLength(mode);
|
||||||
|
if (dataLength < 1)
|
||||||
|
throw "Invalid data length: " + dataLength;
|
||||||
|
switch (mode) {
|
||||||
|
case MODE_NUMBER: // 数字模式
|
||||||
|
str += reader.getFigureString(dataLength);
|
||||||
|
break;
|
||||||
|
case MODE_ROMAN_AND_NUMBER: // 字母数字模式
|
||||||
|
str += reader.getRomanAndFigureString(dataLength);
|
||||||
|
break;
|
||||||
|
case MODE_8BIT_BYTE: // 8bit 字节模式,先判断编码方式(ECI/BOM/启发式)再解码
|
||||||
|
str += bytesToString(reader.get8bitByteArray(dataLength), eci);
|
||||||
|
break;
|
||||||
|
case MODE_KANJI: // 汉字(日文)模式
|
||||||
|
str += reader.getKanjiString(dataLength);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw "Invalid mode: " + mode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// var end = new Date().getTime();
|
// var end = new Date().getTime();
|
||||||
|
|||||||
+2
-2
@@ -32,8 +32,8 @@ Decoder.correctErrors = function (codewordBytes, numDataCodewords) {
|
|||||||
|
|
||||||
Decoder.decode = function (bits) {
|
Decoder.decode = function (bits) {
|
||||||
var parser = new BitMatrixParser(bits);
|
var parser = new BitMatrixParser(bits);
|
||||||
var version = parser.readVersion();
|
var version = parser.readVersion(); //版本信息
|
||||||
var ecLevel = parser.readFormatInformation().ErrorCorrectionLevel;
|
var ecLevel = parser.readFormatInformation().ErrorCorrectionLevel; //格式信息
|
||||||
|
|
||||||
// Read codewords
|
// Read codewords
|
||||||
var codewords = parser.readCodewords();
|
var codewords = parser.readCodewords();
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
// 综合回归测试:标准图 + 特殊图 + 构造场景(策略链各层)
|
||||||
|
// 用法:node test/all_regression.js
|
||||||
|
var server = require('../server');
|
||||||
|
var imgDecode = require('../src/imageDecode');
|
||||||
|
var qrDecode = require('../src/QRDecode');
|
||||||
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
|
|
||||||
|
var passCount = 0;
|
||||||
|
var failCount = 0;
|
||||||
|
|
||||||
|
function check(label, actual, expected) {
|
||||||
|
if (actual === expected) {
|
||||||
|
passCount++;
|
||||||
|
console.log('PASS ' + label + ' => ' + actual);
|
||||||
|
} else {
|
||||||
|
failCount++;
|
||||||
|
console.log('FAIL ' + label + ' => ' + actual + '(期望 ' + expected + ')');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 文件用例:标准图 + 特殊图 ----------
|
||||||
|
var fileCases = [
|
||||||
|
{ path: './img/16.bmp', label: 'bmp16', expect: '12345' },
|
||||||
|
{ path: './img/24.bmp', label: 'bmp24', expect: '12345' },
|
||||||
|
{ path: './img/32.bmp', label: 'bmp32', expect: '12345' },
|
||||||
|
{ path: './img/lx.jpg', label: 'jpg_lx', expect: '12345' },
|
||||||
|
{ path: './img/yh.jpg', label: 'jpg_yh', expect: '12345' },
|
||||||
|
{ path: './img/8.png', label: 'png8', expect: '12345' },
|
||||||
|
{ path: './img/24.png', label: 'png24', expect: '12345' },
|
||||||
|
{ path: './img/jt.gif', label: 'gif_jt', expect: '12345' },
|
||||||
|
{ path: './img/dt.gif', label: 'gif_dt', expect: '12345' },
|
||||||
|
{ path: './img/otsu.png', label: 'otsu.png 渐变光照', expect: '中国人' },
|
||||||
|
{ path: './img/o1.png', label: 'o1.png 透明背景美化码', expect: '中国人' },
|
||||||
|
{ path: './img/o2.png', label: 'o2.png 深色背景美化码', expect: '中国人' },
|
||||||
|
{ path: './img/o3.png', label: 'o3.png 低对比度图', expect: 'https://mpl.qzz.io/' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---------- 构造场景用例:验证策略链各层 ----------
|
||||||
|
// 基于 8.png 构造,覆盖:Otsu / 反转 / 反转+Otsu / 数据未污染
|
||||||
|
var img8 = (function () {
|
||||||
|
var buffer = fs.readFileSync(path.join(__dirname, './img/8.png'));
|
||||||
|
var imageData = imgDecode.png(buffer);
|
||||||
|
if (Array.isArray(imageData)) imageData = imageData[0];
|
||||||
|
return imageData;
|
||||||
|
})();
|
||||||
|
|
||||||
|
var builtCases = [];
|
||||||
|
|
||||||
|
// 暗化图:RGB × 0.5 -> 需要 Otsu 策略
|
||||||
|
(function () {
|
||||||
|
var data = new Uint8ClampedArray(img8.data.length);
|
||||||
|
for (var i = 0; i < img8.data.length; i += 4) {
|
||||||
|
data[i] = img8.data[i] * 0.5 | 0;
|
||||||
|
data[i + 1] = img8.data[i + 1] * 0.5 | 0;
|
||||||
|
data[i + 2] = img8.data[i + 2] * 0.5 | 0;
|
||||||
|
data[i + 3] = img8.data[i + 3];
|
||||||
|
}
|
||||||
|
builtCases.push({ img: { data: data, width: img8.width, height: img8.height }, label: '暗化图(Otsu)', expect: '12345' });
|
||||||
|
})();
|
||||||
|
|
||||||
|
// 反色码:黑底白码 -> 需要反转策略
|
||||||
|
(function () {
|
||||||
|
var data = new Uint8ClampedArray(img8.data.length);
|
||||||
|
for (var i = 0; i < img8.data.length; i += 4) {
|
||||||
|
var gray = (img8.data[i] * 33.33 + img8.data[i + 1] * 33.33 + img8.data[i + 2] * 33.33) / 100;
|
||||||
|
var v = gray <= 153 ? 255 : 0;
|
||||||
|
data[i] = v; data[i + 1] = v; data[i + 2] = v; data[i + 3] = 255;
|
||||||
|
}
|
||||||
|
builtCases.push({ img: { data: data, width: img8.width, height: img8.height }, label: '反色码(反转)', expect: '12345' });
|
||||||
|
})();
|
||||||
|
|
||||||
|
// 暗反色码:黑底灰模块 -> 需要反转+Otsu 策略
|
||||||
|
(function () {
|
||||||
|
var data = new Uint8ClampedArray(img8.data.length);
|
||||||
|
for (var i = 0; i < img8.data.length; i += 4) {
|
||||||
|
var gray = (img8.data[i] * 33.33 + img8.data[i + 1] * 33.33 + img8.data[i + 2] * 33.33) / 100;
|
||||||
|
var v = gray <= 153 ? 95 : 8;
|
||||||
|
data[i] = v; data[i + 1] = v; data[i + 2] = v; data[i + 3] = 255;
|
||||||
|
}
|
||||||
|
builtCases.push({ img: { data: data, width: img8.width, height: img8.height }, label: '暗反色码(反转+Otsu)', expect: '12345' });
|
||||||
|
})();
|
||||||
|
|
||||||
|
// 原图再识别:验证预处理未污染原始数据
|
||||||
|
builtCases.push({ img: img8, label: '原图再识别(数据未污染)', expect: '12345' });
|
||||||
|
|
||||||
|
// ---------- 执行 ----------
|
||||||
|
var tasks = [];
|
||||||
|
fileCases.forEach(function (c) {
|
||||||
|
tasks.push(server.decodeByPath(path.join(__dirname, c.path))
|
||||||
|
.then(function (txt) { check(c.label, txt, c.expect); })
|
||||||
|
.catch(function (e) { failCount++; console.log('FAIL ' + c.label + ' => ' + e); }));
|
||||||
|
});
|
||||||
|
builtCases.forEach(function (c) {
|
||||||
|
tasks.push(Promise.resolve().then(function () {
|
||||||
|
try { check(c.label, qrDecode(c.img), c.expect); }
|
||||||
|
catch (e) { failCount++; console.log('FAIL ' + c.label + ' => ' + e); }
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all(tasks).then(function () {
|
||||||
|
console.log('');
|
||||||
|
console.log('========================');
|
||||||
|
console.log('综合回归:通过 ' + passCount + ' / 失败 ' + failCount);
|
||||||
|
process.exit(failCount > 0 ? 1 : 0);
|
||||||
|
});
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 308 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 187 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
@@ -39,4 +39,8 @@ Promise.resolve()
|
|||||||
.then(function () { return test('./img/24.png', 'png24 ') })
|
.then(function () { return test('./img/24.png', 'png24 ') })
|
||||||
.then(function () { return test('./img/jt.gif', 'gif_jt') }) //单帧
|
.then(function () { return test('./img/jt.gif', 'gif_jt') }) //单帧
|
||||||
.then(function () { return test('./img/dt.gif', 'gif_dt') }) //多帧
|
.then(function () { return test('./img/dt.gif', 'gif_dt') }) //多帧
|
||||||
|
.then(function () { return test('./img/otsu.png', 'otsu ') }) //渐变光照图
|
||||||
|
.then(function () { return test('./img/o1.png', 'o1 ') }) //透明背景美化码
|
||||||
|
.then(function () { return test('./img/o2.png', 'o2 ') }) //深色背景美化码
|
||||||
|
.then(function () { return test('./img/o3.png', 'o3 ') }) //低对比度图
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user