资讯动态

QRDECODE识别率卡七成:二值化、单应矩阵与RS纠错排查

发布时间:2026/10/26 13:43:11 来源:尧图企业网站定制
简介面向在 Windows 平台使用 Visual C 与 MFC 开发二维码识别功能的开发者这份工程示例以 ZBar 解码库为核心演示从图像数据读取、符号扫描到解析结果输出的完整链路并兼顾对话框界面的结果展示适合具备 C 基础、希望快速理解 VC 集成第三方图像处理库流程的中级学习者参考。压缩包共 23 个文件约 34KB以 h 头文件与 cpp 源文件为主体配套 dsw、dsp 工程文件、rc 资源脚本及 ico 图标等保留了一个可直接梳理的 MFC 工程结构便于对照阅读模块划分与编译配置。已有 729 人学习。读者可从中了解 ZBar 头文件与库的引用方式、扫描器对象的调用逻辑、错误处理位置以及界面控件更新思路为后续扩展实时扫描、图像预处理与多线程优化提供可复用的工程起点。1. 二维码解析为什么 QRDECODE 接进去之后识别率还是卡在七成在 VC 里把一个 QRDECODE 之类的解码模块塞进 MFC 工程编译通过、单张测试图能出结果很多人到这里就认为二维码解析已经做完了。真正批量跑起来才发现识别率卡在七成上下剩下那三成既不复现也不报错——它只是安静地返回失败。这个落差值得追究二维码解码从来不是“图像识别”问题而是一次几何测量加一次纠错译码。难点不在“看见码”而在把采样点误差压到半个模块以内压不住后面的 Reed-Solomon 再强也救不回来。往下拆从灰度、二值化、Finder Pattern 定位、单应变换一路到格式信息校验和 MFC 位图内存的 stride每一层都可能成为那个安静的失败。2. 二值化与定位三个 Finder Pattern 是怎么被扫描线“扫”出来的2.1 灰度化为什么加权平均比 (RGB)/3 更靠谱二维码解码的第一步永远是把彩色位图压成单通道灰度。这一步看起来最没有技术含量恰恰是最容易埋雷的地方。人眼对绿光的敏感度远高于蓝光同样的物理亮度绿色看起来亮得多所以 ITU-R BT.601 给出的亮度权重是 0.299R 0.587G 0.114B。用整数近似就是 77/150/29右移 8 位一次乘加就能算完不需要浮点。如果偷懒用 (RGB)/3红底黑码和蓝底黑码算出来的灰度值会明显偏离人眼感知二值化阈值一刀切下去某一种底色的码会整片糊掉。这种问题在测试集上不一定暴露因为测试图往往是白底黑码一旦换成深色背景或者彩色海报识别率就断崖式下跌还很难定位原因。注意Windows 的 DIB 像素顺序是 BGRA不是 RGBA。写灰度化循环时把 src[0] 当红色是最典型的踩坑结果是蓝色通道被当成亮度主力深蓝底色的码直接消失。2.2 全局阈值 Otsu 与局部自适应阈值什么时候必须换算法Otsu 的思路是遍历 0255 的每一个灰度作为候选阈值把直方图切成前景和背景两类计算类间方差取方差最大的那个阈值。它的隐含假设是整幅图光照均匀、前景背景各自成一个单峰。对于扫描枪截出来的干净图Otsu 又快又准几行代码就能实现。但这个假设在手机拍摄场景下几乎必然失效。一张 A4 纸放在台灯下左上角过曝到 250右下角落在阴影里只有 60全局阈值无论取多少总有一半区域被判错。这时候必须换成局部自适应阈值对每个像素取它周围 w×w 窗口内的均值 mean若像素值小于 mean×(1-t) 就判为黑t 一般取 0.15 左右。直接对每个像素求窗口均值是 O(W×H×w²)太慢。工程上的标准做法是积分图Summed Area Table先对整幅图做一次前缀和之后任意矩形区域的像素和都是四次查表加减复杂度降到 O(W×H)与窗口大小无关。// 积分图 Bradley 自适应阈值O(W*H)与窗口半径无关 void AdaptiveThreshold(const uint8_t* gray, int w, int h, int stride, std::vectoruint8_t bin, int radius, double t) { // 积分图多一行一列方便处理边界避免判断 x0 / y0 std::vectoruint32_t sat((size_t)(w 1) * (h 1), 0); for (int y 0; y h; y) { uint32_t rowSum 0; const uint8_t* src gray (size_t)y * stride; for (int x 0; x w; x) { rowSum src[x]; // sat[y1][x1] sat[y][x1] 本行前缀和 sat[(size_t)(y 1) * (w 1) (x 1)] sat[(size_t)y * (w 1) (x 1)] rowSum; } } bin.assign((size_t)stride * h, 255); for (int y 0; y h; y) { int y0 std::max(0, y - radius), y1 std::min(h - 1, y radius); uint8_t* dst bin.data() (size_t)y * stride; for (int x 0; x w; x) { int x0 std::max(0, x - radius), x1 std::min(w - 1, x radius); uint32_t sum sat[(size_t)(y1 1) * (w 1) (x1 1)] - sat[(size_t)y0 * (w 1) (x1 1)] - sat[(size_t)(y1 1) * (w 1) x0] sat[(size_t)y0 * (w 1) x0]; int area (x1 - x0 1) * (y1 - y0 1); // 低于窗口均值的 (1-t) 倍判黑t 越大越保守 if (gray[(size_t)y * stride x] * area sum * (1.0 - t)) dst[x] 0; } } }radius决定“局部”有多局部经验值是图像短边的 1/161/8太小会把纸面纹理当成模块太大会退化成全局阈值。t控制灵敏度取值在 0.10.2 之间遇到浅色打印或褪色标签可以调小。自适应阈值副作用是噪点变多后面的扫描线检测必须依靠“1:1:3:1:1”这种强几何约束来滤掉误报不能指望二值图干净。2.3 1:1:3:1:1 扫描线与 Finder Pattern 的几何约束二维码三个角上的定位图形是一个 7×7 的同心方块黑 1、白 1、黑 3、白 1、黑 1。任何一条水平穿过它中心的扫描线黑白游程长度必然近似成 1:1:3:1:1 的比例。这个比例就是整个定位环节的核心判据也是二维码能在任意旋转角度下被找到的原因——它不依赖朝向只依赖比例。实际实现时逐行遍历二值图维护最近五段游程的长度。每当游程从黑变白或从白变黑就检查这五段是否满足比例。// 判断五段游程是否满足 1:1:3:1:1runs[0..4] 黑,白,黑,白,黑 static bool MatchFinderRatio(const int runs[5], double moduleSize) { int total runs[0] runs[1] runs[2] runs[3] runs[4]; if (total 7) return false; // 至少 7 个模块宽再小没法信 double m total / 7.0; // 单个模块的估计宽度 double tol m * 0.6; // 容差随模块宽度线性放大 if (fabs(runs[0] - m) tol) return false; if (fabs(runs[1] - m) tol) return false; if (fabs(runs[2] - 3 * m) 3 * tol) return false; // 中间 3 模块给 3 倍容差 if (fabs(runs[3] - m) tol) return false; if (fabs(runs[4] - m) tol) return false; moduleSize m; return true; }tol取 0.6 个模块宽度是个折中取得太小透视变形和模糊会让真定位图形失配取得太大二维码内部数据区里偶然出现的 1:1:3:1:1 形状会大量误报。中间那 3 个模块给三倍容差是因为它是整条线上最长的一段绝对误差本来就更大如果用同样的容差稍远的码就会漏检。单条扫描线命中的点叫候选中心点位置取中间那段黑游程的中点。真实的定位图形会被几十上百条扫描线命中所以下一步是做聚类把所有候选点按 (x, y, moduleSize) 三维聚合同一簇里点数超过阈值、且 moduleSize 方差小的才算一个真定位图形。这一步是滤除误报的主力。聚类之后还得再做两道校验。第一道是垂直方向复检以候选中心为起点沿垂直方向重新统计一次 1:1:3:1:1比例不成立就丢弃——这条能干掉绝大多数横向拉伸的误报。第二道是三点成直角等腰三角形三个定位图形确定后两两距离中最长的应该是另外两条的 √2 倍且三个角里有一个接近 90 度。这两道校验加起来误报率能降到可以忽略的量级。提示如果发现某些图尤其是带大面积深色背景的海报连一个定位图形都找不到先别改比例容差去检查二值化。自适应阈值在纯色大块区域会把整片判成黑或整片判成白游程统计直接失效。这类图的正确解法是先做一次全局 Otsu 判断极性再局部细化。3. 透视校正为什么摆正一张歪图必须动用单应矩阵3.1 八个未知数单应矩阵为什么刚好要四对点找到三个定位图形之后很多人第一反应是“把图像旋转一下”。这是错的。手机拍摄的二维码几乎必然带透视——纸面不是正对镜头远处小近处大四个角构成的不是一个矩形而是一个任意四边形。旋转只能纠正刚体变换纠正不了透视收缩。透视变换的数学模型是单应矩阵 H一个 3×3 矩阵作用在齐次坐标上[x] [h0 h1 h2] [x] [y] [h3 h4 h5] * [y] [w] [h6 h7 h8] [1]真实像素坐标是 (x/w, y/w)。矩阵有 9 个元素但整体缩放不改变映射结果所以只有 8 个自由度h8 可以直接定为 1。8 个未知数需要 8 个方程一对点提供 2 个方程所以刚好四对点。二维码的三个定位图形中心加一个校正图形版本 2 以上都有正好凑够四对。求解就是解一个 8×8 的线性方程组。直接用高斯消元能算但矩阵条件数可能很差尤其是四个点接近共线时。工程上通行的做法是先做坐标归一化把源点和目标点各自平移到质心、缩放到平均距离为 √2解完再把 H 乘回去。这一步能把数值稳定性提升几个数量级代码量却只有几十行。四对点的对应关系是事先知道的三个定位图形中心在模块坐标系里分别是 (3.5, 3.5)、(N-3.5, 3.5)、(3.5, N-3.5)其中 N 17 4×version 是每边的模块数右下角校正图形中心在 (N-6.5, N-6.5)。把这些模块坐标和图像上实测的像素坐标配对解出来的 H 就是模块坐标到像素坐标的映射。3.2 版本推断与模块网格对齐解 H 之前得先知道 N也就是版本号。这里有个挺好用的几何关系两个定位图形中心的距离在模块坐标系里恒等于 N-7也就是 10 4×version 个模块。而在像素坐标系里这个距离可以直接用两点欧氏距离算出来再除以之前估计的 moduleSize就得到模块数。version round(((distPixels / moduleSize) - 10) / 4)算出来的版本号必须落在 140 之间超出就说明 moduleSize 估错了得回到定位阶段重新聚类。这个公式的容错性其实不高distPixels 有 3% 误差版本号就可能跳一档。所以更稳的做法是先用这个粗估版本确定一个搜索范围比如 ±2再用后续的格式信息校验来反推真实版本——因为格式信息的 BCH 校验只有 32 个合法码字一旦匹配上版本和纠错级别就同时确定了。版本确定后网格对齐还有一件事要做模块坐标 (mx, my) 对应的采样点是 (mx 0.5, my 0.5)因为整数坐标落在模块的左上角中心要偏移半个模块。这个 0.5 忘掉采样点就系统性偏移半个模块在模块宽度只有 3 个像素的小图上等于直接采到了隔壁模块。3.3 采样双线性插值和“往中心缩一点”的经验有了 H采样就是纯计算。对每个模块中心做透视映射得到浮点像素坐标然后在这个位置上取值。最近邻最快但在模块边界附近容易采错双线性插值多算三次乘加稳定性明显更好。struct Homography { double h[9]; }; // 把模块坐标映射回原图像素坐标双线性采样返回 0255 灰度 inline int SampleModule(const uint8_t* gray, int w, int h, int stride, const Homography H, double mx, double my) { double den H.h[6] * mx H.h[7] * my H.h[8]; if (fabs(den) 1e-12) return 255; // 退化情况直接判白 double px (H.h[0] * mx H.h[1] * my H.h[2]) / den; double py (H.h[3] * mx H.h[4] * my H.h[5]) / den; int x (int)floor(px), y (int)floor(py); if (x 0 || y 0 || x 1 w || y 1 h) return 255; // 出界判白 double fx px - x, fy py - y; const uint8_t* p gray (size_t)y * stride x; double v (1 - fx) * (1 - fy) * p[0] fx * (1 - fy) * p[1] (1 - fx) * fy * p[stride] fx * fy * p[stride 1]; return (int)(v 0.5); }den是齐次坐标的 w 分量接近 0 意味着这个点被映射到无穷远物理上不可能出现出现了说明 H 解错了直接判白让纠错去处理。越界判白同理二维码外面的静区本来就是白的。采样完之后每个模块得到一个灰度值再和该模块所在局部区域的阈值比较得到 0/1。这里有个小技巧不要用全局阈值而是用这个模块在 H 反算回原图后 3×3 邻域的均值做局部判断能明显提升低对比度图片的成功率。更进一步的做法是每三个像素采一次取多数票代价是计算量翻倍收益在这类图上大概能换来几个百分点的识别率。注意采样阶段的任何“差不多”都会被后面放大。一个模块采错可能导致整个码字8 个模块错位进而吃掉一个纠错单元。RS 纠错能救的是随机错误救不了系统性的网格偏移。4. 格式信息与 Reed-Solomon解码器真正的“后悔药”藏在哪4.1 Format Info 的 BCH(15,5)为什么同一份信息要写两遍采样得到的 0/1 矩阵还不是数据得先知道两件事用什么级别的纠错、用了哪个掩码。这两条信息合起来 5 个比特2 位纠错级别 3 位掩码编号编码成 15 位用 BCH(15,5) 加 10 位校验位。生成多项式是 0x537最后再整体异或一个固定掩码 0x5412——这个异或是为了防止全 0 的格式信息合法但容易和空白区域混淆出现。关键在于这 15 位在码图里写了两遍。第一份在左上角定位图形周围第二份拆成两半一半在右上角、一半在左下角。冗余的意义很直接——只要有一份完整读出来格式信息就确定了。实际解码时更省事的做法不是去跑 BCH 译码而是把 32 个合法码字全部枚举出来和读到的 15 位逐一算汉明距离取距离最小的那个距离大于 3 就认为读失败。32 次异或加查表比实现一个完整 BCH 译码器简单得多速度也够。纠错级别的两位编码是 L01、M00、Q11、H10不是按顺序排的第一次写容易写反。这四档对应的纠错能力大约是 7%、15%、25%、30% 的码字可以被恢复。4.2 掩码不是“美化”是为了打散伪 Finder Pattern八种掩码图案每一种都是对模块坐标 (i, j) 做一次取模运算结果为 0 的位置翻转编号条件直观形态000(i j) mod 2 0棋盘格001i mod 2 0横条纹010j mod 3 0竖条纹011(i j) mod 3 0斜条纹100(⌊i/2⌋ ⌊j/3⌋) mod 2 0粗块101(i·j) mod 2 (i·j) mod 3 0混合110((i·j) mod 2 (i·j) mod 3) mod 2 0混合111((ij) mod 2 (i·j) mod 3) mod 2 0混合为什么要掩码因为原始数据里如果出现连续大片同色或者偶然形成类似定位图形的形状解码器的扫描线就会被骗。编码端会把八种掩码都试一遍按四条惩罚规则打分连续同色长度、2×2 同色块数量、类似定位图形的出现次数、黑白比例偏离度选分数最低的那个。解码端反而简单格式信息里已经告诉你用了哪个掩码直接按同一公式再异或一次就还原了——异或两次等于没做。这里不需要重新打分因为打分是编码端的事解码端只需要知道结果。4.3 数据块交织与 GF(256) 上的 RS 纠错去掉掩码之后数据区里的比特流要按“块”重新组织。二维码会根据版本和纠错级别把整个数据区分成若干个数据块每块配若干纠错码字然后把所有块的数据码字按列交织排列纠错码字同样交织排列在后面。交织的目的很明确物理污损往往是局部的、连续的一张纸被划了一道连续几十个模块都错了。如果不交织这些错误会全部落在同一个块里直接超出该块的纠错能力交织之后连续错误被打散到各个块每块只错两三个字节很容易被纠回来。纠错本身是 GF(256) 上的 Reed-Solomon。GF(256) 的加法就是异或乘法用本原多项式 0x11D 做模运算。解码第一步是算伴随式// GF(256) 乘法本原多项式 x^8x^4x^3x^21 0x11D static uint8_t gf_mul(uint8_t a, uint8_t b) { int r 0; while (b) { if (b 1) r ^ a; a (a 1) ^ ((a 0x80) ? 0x1D : 0); // 溢出时减去本原多项式 b 1; } return (uint8_t)r; } // 计算伴随式 S[i] R(alpha^i)i 0..nsym-1QR 的根从 alpha^0 开始 bool ComputeSyndromes(const uint8_t* blk, int n, int nsym, uint8_t* S) { bool clean true; for (int i 0; i nsym; i) { uint8_t x gf_pow_alpha(i); // alpha 的 i 次幂alpha 2 uint8_t acc 0; for (int j 0; j n; j) // 霍纳法则一趟扫完多项式求值 acc (uint8_t)(gf_mul(acc, x) ^ blk[j]); S[i] acc; if (acc ! 0) clean false; } return clean; }nsym是该块里纠错码字的数量由版本和纠错级别查表得到。如果所有伴随式都为 0说明这块数据完好直接跳过后续步骤——这是最常见的路径也是整个解码里最该优化的分支。伴随式非零时才需要走 Berlekamp-Massey 求错误定位多项式、Chien 搜索找错误位置、Forney 算法算错误值这三步。纠错能力是 ⌊nsym/2⌋ 个字节错误超出这个数量的块只能丢弃。译码阶段还要处理模式指示符前 4 位告诉你这是数字模式0001、字母数字模式0010、字节模式0100还是日文汉字模式1000。字节模式最常见也最容易翻车因为它只规定了“一串字节”没规定编码——同样一段中文可能是 UTF-8也可能是 GBK还可能有 ECI 指示符声明字符集。没有 ECI 时的默认行为是本地代码页这就是中文乱码的根源。5. 排查清单MFC 工程里二维码识别失败的几类典型翻车5.1 现象预览里清清楚楚解出来全是噪声现象把同一张图用图片查看器打开完全正常喂给解码函数得到的结果是满屏随机数据或者定位阶段直接失败。原因MFC 里从 CImage 或 CBitmap 取像素最容易错的是三件事——位深不是 32 位却按 4 字节步进、每行字节数没按 4 字节对齐、DIB 的 biHeight 为正导致自上而下和自下而上颠倒。任何一条出错读到的都是错行的数据看上去“有图”实际上是乱的。解决统一转成 32 位再处理逐行按 stride 拷贝不要假设 width×4 就是 stride。// CImage - 灰度缓冲stride 按 4 字节对齐GetPixelAddress 已处理 DIB 方向 bool ToGray(const CImage img, std::vectoruint8_t gray, int w, int h, int stride) { if (img.IsNull()) return false; w img.GetWidth(); h img.GetHeight(); stride (w 3) ~3; // 4 字节对齐方便后续按行处理 gray.assign((size_t)stride * h, 255); int bpp img.GetBPP() 3; // 每像素字节数1/2/3/4 for (int y 0; y h; y) { // GetPixelAddress 内部已经考虑了 biHeight 的符号不要自己算行首 const BYTE* src (const BYTE*)img.GetPixelAddress(0, y); uint8_t* dst gray.data() (size_t)y * stride; for (int x 0; x w; x, src bpp) { // DIB 顺序是 BGR(A)不是 RGB别写反 dst[x] (uint8_t)((src[0] * 29 src[1] * 150 src[2] * 77) 8); } } return true; }bpp从图像的位深动态取兼容 8/24/32 位输入灰度用 29/150/77 的整数权重跟 BT.601 对齐。这段代码跑通之后先用一张已知正确的图打印灰度直方图确认别急着往下调定位参数。5.2 现象截图能解手机拍的照片一张都解不了现象用系统截图工具截的二维码图识别率接近 100%换成手机拍摄的照片成功率掉到 20% 以下且失败的图往往带反光或者阴影。原因全局阈值在光照不均时失效。更隐蔽的是高光反射——塑料标签、覆膜纸张、手机屏幕上的二维码某一块区域直接过曝到 255黑模块被冲成白色任何二值化都救不回来局部信息。解决换成积分图自适应阈值窗口半径取短边的 1/8 左右同时在二值化之前做一次轻度高斯模糊3×3 就够把过曝边缘的硬跳变压一压。如果失败集中在高光区可以在定位阶段放宽比例容差靠后面的 RS 纠错去兜底——只要三个定位图形还在局部数据损坏是完全可恢复的。5.3 现象小图识别率断崖式下跌现象原图 2000 像素宽时识别正常缩到 200 像素宽就基本全失败明明肉眼还能看清。原因模块宽度不足。当单个模块只占 12 个像素时1:1:3:1:1 的游程统计误差已经和模块本身同量级比例判据失效即使勉强定位成功采样点偏移半个像素就跨到了隔壁模块。解决先判断估算的 moduleSize低于 2.0 就把整幅图用最近邻放大 23 倍再走一遍全流程。这里一定要用最近邻而不是双线性——双线性会把黑模块的边缘抹成灰二值化之后模块宽度反而不稳定。更稳的做法是搭一个简易金字塔从 1×、2×、3× 三个尺度各跑一次任意一个成功就返回。5.4 现象英文和数字都对中文全是乱码现象扫出来的内容里ASCII 部分完全正确中文部分是“锟斤拷”或者一堆问号。原因字节模式只声明“接下来 N 个字节”不声明编码。编码端可能用 UTF-8也可能用 GBK还可能带 ECI 指示符。解码端如果无脑按本地代码页转遇到 UTF-8 编码的中文必然乱。解决按优先级尝试——有 ECI 就按 ECI 指定的字符集没有 ECI 就先按 UTF-8 严格校验检查多字节序列的合法性校验失败再回退到本地代码页。这个顺序能覆盖绝大多数实际场景因为现代生成工具默认输出 UTF-8。5.5 现象偶发崩溃堆栈指向纠错环节现象大部分图正常某些特定版本的码一解就崩调试器停在 RS 纠错或者块重组的循环里。原因版本和纠错级别查出来的“块数”“每块纠错码字数”来自静态表如果版本号因为定位误差被算成了边界值比如算出 0 或 41或者格式信息解出了一个表里没有的组合后续按表索引就越界了。解决所有查表操作前做范围断言版本号必须落在 140纠错级别必须是 03块索引必须小于块总数。同时准备一批覆盖全部 40 个版本、4 个纠错级别的回归样本任何一次改动都跑一遍——这类越界往往只在特定版本上触发样本不全根本发现不了。提示解码函数不要只返回 true/false。把失败阶段定位失败 / 格式信息校验失败 / 块重组失败 / RS 纠错失败一起返回出问题时一眼就能看出卡在哪一层比加一堆日志管用。6. 工程取舍解码器该待在哪个线程失败时该留下什么把解码逻辑塞进 MFC 的按钮响应函数里是绝大多数人写的第一版也是最容易翻车的一版。一张 4000×3000 的手机照片自适应阈值加多尺度搜索单张耗时可以到几百毫秒批量导入一百张界面直接假死用户以为程序崩了。方案单张耗时界面响应实现成本适用场景UI 线程同步解码50800 ms完全阻塞最低单张、演示工作线程 PostMessage同上无阻塞低批量导入线程池 进度回调同上可并行无阻塞中成批处理独立进程解码略高IPC 开销无阻塞崩溃隔离高不可信输入我一般推荐工作线程加PostMessage这一档用AfxBeginThread起一个AfxThreadEntry把CImage的路径不是CImage对象本身传进去线程内自己加载、解码完成后PostMessage(WM_DECODE_DONE, ...)把结果送回 UI 线程。注意别用SendMessage——跨线程SendMessage会阻塞工作线程直到 UI 处理完一旦 UI 正在忙其他事就形成互相等待。也别把 GDI 对象跨线程传递GDI 的初始化状态和 DC 是有线程亲和性的老老实实在线程内加载图片。另一个容易被忽略的取舍是“失败时该留下什么”。早期的实现里解码失败就返回false结果现场反馈“扫不出来”时完全没法排查——是图太糊、光太强、还是码本身有问题全靠猜。后来我把中间结果全部落盘二值化后的图、定位到的三个中心点坐标、解出的格式信息、每个块的伴随式是否为零。这套东西相当于给解码器装了个黑匣子绝大多数问题看一眼二值图就能定位到是哪一环出的问题。再往深一层还有个值得权衡的点解码器要不要做“尽力而为”。有些实现一旦某个块 RS 纠错失败就整体放弃但二维码的块是独立的某一块坏了并不影响其余块——只要模式指示符和长度字段所在的块是好的剩下的数据完全可以部分还原出来末尾用占位符标注。对于扫码这种场景拿到 80% 的内容也比拿到 0 强得多。是否开启这个模式取决于业务能不能接受不完整数据这是产品决策而不是技术决策。从那以后我每次接入一个新的解码模块都会先跑一套固定的样本集白底黑码、深色底、旋转 45 度、透视倾斜、局部高光、模块宽度 1 像素、40 个版本各一张。这套样本跑不通就不进业务代码——血泪经验是靠肉眼挑几张图测出来的识别率和真实场景差得远。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑