简介:ZXing.Net.0.16.8.0 是面向 C# 与 Unity 开发者的开源条码处理库,基于 Zebra Crossing 项目,可在 .NET 框架与 Unity 引擎中实现 QR 码、Aztec、Code 128、EAN 等多种条码的读取与生成。开发者只需调用少量 API 即可为桌面应用、移动端或游戏加入扫码功能,例如让玩家扫描屏幕条码获取游戏内物品信息。压缩包共 132 个文件,约 18.27MB,以 dll 动态库、pdb 调试符号、xml 文档注释为主,另含 pri、winmd、cmd 脚本及 COPYING、THANKS 等说明文件,覆盖 net4.0、net4.6、net5.0、sl4、sl5、wp8.0、ce2.0 等多个平台版本,便于按目标框架挑选依赖。目前已有 923 人学习下载,适合需要跨平台条码识别能力的中高级 C# 与 Unity 开发者参考,可帮助快速完成库的选型、导入与兼容性适配。
1. 从一次扫码翻车说起:ZXing.Net 0.16.8.0 到底能解决什么
去年帮一个做仓储盘点的团队看问题,他们的 Unity 项目在 PC 编辑器里扫码识别率接近百分之百,打包到 Android 真机后,同一张二维码十次里能失败六七次。排查了两天,最后发现是摄像头采集的帧格式和识别库之间的色彩空间没对齐,跟算法本身没关系。那次之后我养成了一个习惯:凡是涉及条码识别的项目,先把底层库的版本、依赖和平台差异摸清楚,再谈业务逻辑。ZXing.Net 0.16.8.0 就是这样一个值得先摸清楚的底层库——它是 ZXing(Zebra Crossing)的 .NET 移植版本,支持 QR Code、Data Matrix、Code 128、EAN-13 等主流一维和二维条码的生成与解码,可以跑在 .NET Framework、.NET Core、.NET Standard 以及 Unity 的 C# 运行时里。如果你正在用 Unity 做扫码登录、库存管理、票务核验,或者需要在服务端批量生成条码图片,这个包基本是绕不开的选项。它适合两类人:一类是刚接触条码识别、想找一个能直接跑通的 C# 方案的新手;另一类是已经用过其他库、但被平台兼容性或识别率折腾过的熟手。接下来我会把它的核心用法、参数调优和几个容易翻车的地方拆开讲。
2. 核心解码链路:从图像到文本的完整实现
2.1 为什么选 ZXing.Net 而不是自己写解析
条码识别看起来简单——找到黑白模块、按规则翻译成字符——但真正落地时会遇到一堆工程问题:图像有透视畸变怎么办、光照不均导致二值化阈值失效怎么办、多个条码同时出现在画面里怎么定位、不同码制的纠错等级怎么处理。ZXing 内部把这些都封装成了可配置的 Pipeline,包括灰度化、二值化、条码区域检测、网格采样、纠错解码几个阶段。自己从零实现一套能稳定跑的,保守估计要投入几个月,而且很难覆盖所有码制。ZXing.Net 0.16.8.0 在这个基础上做了 .NET 侧的适配,把 Bitmap 和 Unity 的 Color32 数组之间的转换、多线程解码、结果回调都准备好了。常见做法是直接引用它的核心库,然后在自己的采集层做帧格式转换,把图像喂给 BarcodeReader 就行。
2.2 最小可运行示例:解码一张本地图片
先看最基础的用法,从一张 PNG 图片里读出二维码内容。这段代码在 .NET Core 控制台项目和 Unity 的 Editor 脚本里都能跑。
using ZXing; using System.Drawing; // .NET Core 需要额外引用 System.Drawing.Common // 创建解码器,指定可能出现的码制 var reader = new BarcodeReader { // 只开启 QR Code,减少误识别 Options = new ZXing.Common.DecodingOptions { PossibleFormats = new List<BarcodeFormat> { BarcodeFormat.QR_CODE }, TryHarder = true, // 对模糊图像更努力地尝试 TryInverted = true // 同时尝试反色图像 } }; // 加载图片并解码 using var bitmap = new Bitmap("qrcode.png"); var result = reader.Decode(bitmap); if (result != null) { Console.WriteLine($"格式: {result.BarcodeFormat}"); Console.WriteLine($"内容: {result.Text}"); // 二维码四个角点,可用于定位或绘制 foreach (var point in result.ResultPoints) { Console.WriteLine($"角点: {point.X}, {point.Y}"); } } else { Console.WriteLine("未识别到条码"); }这段代码的关键在DecodingOptions的三个参数。PossibleFormats限定码制是最容易被忽略的优化点——如果不限制,解码器会依次尝试所有支持的格式,耗时可能翻好几倍,而且容易把纹理误判成条码。TryHarder开启后,解码器会在第一次失败后尝试更复杂的二值化策略和旋转角度,代价是耗时增加,适合对识别率要求高、对延迟不敏感的场景。TryInverted处理白底黑码和黑底白码两种情况,有些工业场景的标签是反色印刷的,不开这个参数就会漏掉。
2.3 Unity 环境下的图像采集与解码适配
Unity 里没有 System.Drawing,图像数据通常是WebCamTexture或Color32[]数组。ZXing.Net 提供了Color32LuminanceSource来直接接收 Unity 的像素数据,省去转 Bitmap 的开销。
using UnityEngine; using ZXing; using System.Collections.Generic; public class QRScanner : MonoBehaviour { private WebCamTexture camTexture; private BarcodeReader reader; private float scanInterval = 0.2f; // 每 200ms 解码一次,避免每帧都跑 private float timer; void Start() { // 初始化摄像头 camTexture = new WebCamTexture(640, 480, 30); camTexture.Play(); // 配置解码器 reader = new BarcodeReader { Options = new ZXing.Common.DecodingOptions { PossibleFormats = new List<BarcodeFormat> { BarcodeFormat.QR_CODE }, TryHarder = true } }; } void Update() { timer += Time.deltaTime; if (timer < scanInterval || !camTexture.isPlaying) return; timer = 0; // 获取当前帧像素 Color32[] pixels = camTexture.GetPixels32(); int width = camTexture.width; int height = camTexture.height; // 直接传入 Color32 数组,内部会做灰度化 var source = new Color32LuminanceSource(pixels, width, height); var result = reader.Decode(source); if (result != null) { Debug.Log($"识别成功: {result.Text}"); // 触发业务逻辑,比如打开链接、提交表单 } } void OnDestroy() { if (camTexture != null) camTexture.Stop(); } }这里有几个参数需要根据实际设备调整。scanInterval控制解码频率,移动端每帧解码会迅速吃满 CPU 导致发热降频,200ms 一次是识别速度和功耗的折中。摄像头分辨率 640x480 对二维码识别通常够用,再高会增加像素拷贝和灰度化的耗时。Color32LuminanceSource内部用的是标准灰度公式0.299R + 0.587G + 0.114B,如果你的场景光照偏色严重,可以考虑先做一次白平衡再传入。
2.4 生成条码:编码参数与输出格式
除了解码,ZXing.Net 也能生成条码。服务端批量出码、Unity 里动态渲染二维码都用得上。
using ZXing; using ZXing.QrCode; using ZXing.Common; // 配置二维码生成参数 var writer = new BarcodeWriterPixelData { Format = BarcodeFormat.QR_CODE, Options = new QrCodeEncodingOptions { Height = 256, Width = 256, Margin = 2, // 静默区宽度,单位是模块数 ErrorCorrection = ZXing.QrCode.Internal.ErrorCorrectionLevel.H, CharacterSet = "UTF-8" // 支持中文内容 } }; // 生成像素数据 var pixelData = writer.Write("https://example.com/order/12345"); // pixelData.Pixels 是 BGRA 格式的字节数组,可以直接创建 Texture2DErrorCorrection设成 H 级别意味着二维码有 30% 的纠错能力,适合印刷后可能磨损或部分遮挡的场景,代价是同样内容需要更多模块,码会变密。Margin是静默区,很多生成库默认给 0,导致扫码器找不到边界,这是个高频翻车点。CharacterSet不设 UTF-8 的话,中文内容会乱码,这个在 0.16.x 版本里需要显式指定。
3. 参数调优与性能边界:让识别率从 70% 到 95%
3.1 二值化策略的选择逻辑
ZXing 内部默认用 HybridBinarizer,它把图像分成 8x8 的块,每块单独计算阈值,适合光照不均匀的场景。但在某些情况下,GlobalHistogramBinarizer 反而更稳——比如图像整体对比度高、没有明显阴影的时候。切换方式是在 DecodingOptions 里设置:
var options = new ZXing.Common.DecodingOptions { PossibleFormats = new List<BarcodeFormat> { BarcodeFormat.QR_CODE }, TryHarder = true }; // 通过 BarcodeReader 的 Binarizer 属性切换 reader.Binarizer = new ZXing.Common.GlobalHistogramBinarizer(source);我一般会先跑一遍默认的 Hybrid,如果识别率不理想,再换 Global 对比。工业视觉场景里,如果打光可控,Global 往往更快更准;移动端手持扫描,Hybrid 适应性更好。
3.2 图像预处理:裁剪、缩放与锐化
ZXing 对输入图像的尺寸有隐式要求。如果图像太大,解码耗时会线性增长;太小则条码模块模糊,采样失败。经验值是二维码在图像中占据的面积不低于 10%,每个模块至少 3 个像素。在 Unity 里可以在传入前做一次中心裁剪:
// 假设原始图像 1280x720,裁剪中心 640x480 区域 int cropW = 640, cropH = 480; int startX = (width - cropW) / 2; int startY = (height - cropH) / 2; Color32[] cropped = new Color32[cropW * cropH]; for (int y = 0; y < cropH; y++) { for (int x = 0; x < cropW; x++) { cropped[y * cropW + x] = pixels[(startY + y) * width + (startX + x)]; } } var source = new Color32LuminanceSource(cropped, cropW, cropH);裁剪减少了解码器需要扫描的像素量,同时把条码区域放大到画面中心,对焦也更准。如果条码本身很小,可以先做一次双线性插值放大再传入,但放大倍数不要超过 2 倍,否则插值带来的模糊反而会降低识别率。
3.3 多码制共存时的优先级与冲突
一个画面里同时出现 QR Code 和 Code 128 时,如果 PossibleFormats 里两个都开着,解码器会按内部顺序尝试,先命中的返回。这可能导致你想读 QR 却读到了旁边的条形码。解决办法有两种:一是根据业务场景只开需要的格式;二是用DecodeMultiple方法拿到所有结果,再按格式筛选。
var results = reader.DecodeMultiple(source); foreach (var r in results) { if (r.BarcodeFormat == BarcodeFormat.QR_CODE) { // 处理二维码 } }DecodeMultiple在 0.16.8.0 里已经支持,但性能比单码解码低不少,只在确实需要多码同读时用。
3.4 线程模型:别在主线程里跑解码
Unity 的 Update 里直接调 Decode 会阻塞渲染。移动端上一帧解码耗时可能到 50-100ms,直接导致掉帧。常见做法是丢到 ThreadPool 或 Task 里跑,主线程只负责采集和渲染结果。
private bool isDecoding = false; void Update() { if (isDecoding || timer < scanInterval) return; timer = 0; isDecoding = true; Color32[] pixels = camTexture.GetPixels32(); int w = camTexture.width, h = camTexture.height; System.Threading.Tasks.Task.Run(() => { var source = new Color32LuminanceSource(pixels, w, h); var result = reader.Decode(source); // 回到主线程更新 UI UnityMainThreadDispatcher.Enqueue(() => { if (result != null) Debug.Log(result.Text); isDecoding = false; }); }); }注意Color32LuminanceSource构造时不会拷贝数组,如果后台线程解码的同时主线程在写 pixels,会有数据竞争。稳妥的做法是在 Task 里先做一次数组拷贝,或者用双缓冲交替采集。
4. 避坑与排查:那些文档里不会写的翻车现场
4.1 现象:PC 上正常,打包到移动端就识别失败
原因:Unity 的 WebCamTexture 在不同平台上返回的像素格式可能不同。PC 上通常是 RGB24,Android 上可能是 YV12 或 NV21,直接 GetPixels32 拿到的数据在部分设备上颜色通道顺序是反的,导致灰度化后黑白颠倒。
解决:在 Start 里检查camTexture.graphicsFormat,如果不是 RGB24,先用WebCamTexture.GetPixels32拿到数据后手动做一次通道交换。或者更稳妥的做法是先用camTexture.GetPixels32()渲染到 RenderTexture,再从 RenderTexture 读取标准 RGB 数据。
4.2 现象:识别率突然下降,之前能扫的码现在扫不出来
原因:ZXing.Net 0.16.8.0 默认的AutoRotate是关闭的。如果摄像头画面旋转了 90 度(比如手机横屏拿但应用锁竖屏),条码在图像里是侧着的,解码器找不到定位图案。
解决:在 DecodingOptions 里设AutoRotate = true,或者自己在传入前把图像旋转到正确方向。AutoRotate 会尝试 0/90/180/270 四个角度,耗时增加约 30%,但能覆盖绝大多数方向问题。
4.3 现象:生成的中文二维码扫出来是乱码
原因:QrCodeEncodingOptions.CharacterSet默认不是 UTF-8,在 0.16.x 里如果不显式设置,中文内容会按 ISO-8859-1 编码写入,扫码端按 UTF-8 解析就乱了。
解决:生成时强制指定CharacterSet = "UTF-8"。另外注意,部分老款扫码 App 对 UTF-8 的支持也不完善,如果目标用户环境不可控,可以考虑把中文内容先做 Base64 编码再写入,扫码后自行解码。
4.4 现象:TryHarder 开了之后 CPU 占用飙升,手机发烫
原因:TryHarder 会在首次解码失败后尝试多种二值化策略和旋转角度,每次尝试都是一次完整的图像扫描。如果画面里根本没有条码,每帧都在做无用功。
解决:加一个前置判断——先用简单的边缘检测或方差计算判断画面里是否有足够的黑白对比区域,没有就直接跳过解码。或者降低解码频率,从每 200ms 一次改成每 500ms 一次,配合 UI 上的扫描框引导用户对准。
4.5 现象:NuGet 安装后编译报错,找不到 ZXing.Common.DecodingOptions
原因:ZXing.Net 0.16.8.0 的 NuGet 包分了多个目标框架版本,Unity 项目如果用的是 .NET Standard 2.0 兼容模式,可能引用到了不包含完整 API 的程序集。
解决:确认 Unity 的 Api Compatibility Level 设为 .NET Standard 2.0 或 .NET 4.x,然后在 NuGet 里安装ZXing.Net主包,不要只装ZXing.Net.Bindings之类的子包。如果用的是 Unity Package Manager,需要手动把 dll 放到 Plugins 目录下,并确保平台设置里勾选了对应的 CPU 架构。
5. 进阶技巧:用自定义 LuminanceSource 接管灰度化
ZXing 默认的灰度化公式对大多数场景够用,但在红外摄像头、低照度或高对比度工业光源下,标准公式可能把条码区域和背景压成同一灰度。这时候可以继承LuminanceSource自己实现灰度映射。
using ZXing; public class CustomLuminanceSource : LuminanceSource { private byte[] luminances; public CustomLuminanceSource(Color32[] pixels, int width, int height) : base(width, height) { luminances = new byte[width * height]; for (int i = 0; i < pixels.Length; i++) { // 自定义权重:增强绿色通道,适合某些工业相机 int gray = (pixels[i].r * 2 + pixels[i].g * 5 + pixels[i].b * 1) / 8; luminances[i] = (byte)Mathf.Clamp(gray, 0, 255); } } public override byte[] GetRow(int y, byte[] row) { int offset = y * Width; if (row == null || row.Length < Width) row = new byte[Width]; for (int x = 0; x < Width; x++) row[x] = luminances[offset + x]; return row; } public override byte[] Matrix => luminances; }这个自定义 Source 把绿色通道权重提高到 5/8,因为多数图像传感器对绿光最敏感,信噪比最高。在低照度环境下,这样加权后条码边缘的梯度更明显,二值化时更容易分离出黑白模块。代价是红色和蓝色信息被压缩,如果条码是红色印刷在白色背景上,反而可能识别不了——所以这个方案适合黑白或深色码,不适合彩色码。
另一个进阶用法是结合BarcodeReader.ResultPointCallback做实时定位反馈。在解码过程中,ZXing 会回调找到的定位点坐标,可以用这些点在 UI 上画扫描框,让用户知道有没有对准。
reader.Options.ResultPointCallback = new ResultPointCallback((point) => { // point 是解码过程中找到的定位点 Debug.Log($"定位点: {point.X}, {point.Y}"); });这个回调在每次尝试解码时都会触发,可以用来做扫描引导动画。注意回调是在解码线程里执行的,更新 UI 需要切回主线程。
从那以后我每次集成条码库,都会先写一个最小测试场景:固定一张标准测试图,跑通解码,再逐步加摄像头采集、多线程、平台适配。这样出问题时能快速定位是库的问题还是采集层的问题。希望帮到你。
本文还有配套的精品资源,点击获取