简介: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 元数据及许可与说明文件,覆盖 net4.0、net4.6、net5.0、wp8.0、sl4/sl5、ce2.0 等多个平台版本,便于按目标框架挑选依赖。目前已有 923 人学习下载。资源提供了完整的跨平台条码能力与版本适配参考,可帮助开发者快速集成扫码模块、排查框架兼容问题,并理解不同 .NET 后端的选型差异。
1. 从一次扫码翻车说起:ZXing.Net 0.16.8.0 到底能解决什么
去年帮一个做仓储盘点的团队调一个 Unity 工具,场景很朴素:手持设备摄像头对着货架上的条码,实时识别出编号回填到列表里。他们一开始用的是某款在线识别服务,网络一抖就卡住,离线仓库里直接罢工。后来换成在客户端本地解码,选的就是 ZXing.Net 这条线。ZXing 本身是 Java 生态里老牌的条码处理库,ZXing.Net 是它在 .NET 平台上的移植版本,0.16.8.0 属于这个移植分支里比较成熟的一个发布号,支持一维码和二维码的生成与解码,纯托管实现,不依赖本地动态库。对 Unity 和 .NET C# 开发者来说,它的价值在于:把「识别条码」这件事从网络请求变成一次本地方法调用,没有服务端成本,也没有网络延迟。适合谁?做 Unity 工具链、桌面端扫码、批量图片解码、需要离线运行的 C# 项目,都值得把它放进工具箱。这一篇不讲条码原理史,只讲这个包怎么落地、参数怎么调、坑在哪。
2. ZXing.Net 0.16.8.0 的组成与选型:为什么是它而不是别的
2.1 包结构与目标框架
ZXing.Net 0.16.8.0 在 NuGet 上是一个主包,内部按功能拆成若干命名空间。核心解码入口是BarcodeReader,编码入口是BarcodeWriter,两者都泛型化到BarcodeReader<T>和BarcodeWriter<T>,T 是像素格式类型。这个设计是为了适配不同渲染后端:桌面端用System.Drawing,Unity 里用Color32,WPF 用WriteableBitmap。0.16.8.0 这个版本对 .NET Standard 的支持已经比较完整,意味着它能同时被 .NET Framework、.NET Core 以及 Unity 的脚本运行时引用。
选它而不是别的方案,理由有三条。第一,纯托管代码,Unity 打包到 IL2CPP 时不会因为本地库缺失而在目标平台崩掉。第二,解码逻辑经过多年打磨,对倾斜、低对比度、部分遮挡的条码容忍度比很多自研实现高。第三,API 面窄,Decode一个方法加几个选项就能跑,学习成本低。常见做法是:先用默认参数跑通,再根据实际图像质量去调DecodingOptions。
2.2 在 Unity 项目里引入的两种方式
Unity 对 NuGet 的原生支持一直是个玄学问题。0.16.8.0 这个包有两种引入路径,我一般按项目类型选。
第一种,项目是纯 .NET 桌面工具,直接用命令行装:
# 在项目根目录执行,安装指定版本 dotnet add package ZXing.Net --version 0.16.8.0这条命令会把包和它的依赖写进.csproj。参数--version必须显式指定,否则会拉到最新版,而最新版的 API 可能和 0.16.8.0 有差异。装完后using ZXing;就能用。
第二种,项目是 Unity,不能直接dotnet add。做法是把 NuGet 包下载下来,解压出lib目录下对应目标框架的 dll,放进Assets/Plugins。这里有个关键点:Unity 的脚本运行时版本要和 dll 的目标框架匹配。如果 Unity 用的是 .NET Standard 2.0,就选netstandard2.0目录下的 dll;如果用的是 .NET 4.x,就选net45或net461。放错目录,编译期就报找不到类型。
// 引入核心命名空间 using ZXing; using ZXing.Common; // 创建一个针对 Color32 像素格式的读取器 // Unity 的 Texture2D.GetPixels32() 返回的正是 Color32[] var reader = new BarcodeReader<Color32>( // 把 Color32 转成 ZXing 内部需要的亮度值 color => (byte)((color.r * 0.299 + color.g * 0.587 + color.b * 0.114)) );这段代码里,构造函数的委托参数是灰度转换函数。ZXing 内部解码只关心亮度,不关心颜色,所以要把 RGB 压成单通道。系数 0.299、0.587、0.114 是标准的亮度加权,别随手改成三等分,否则红色条码的对比度会被算错,识别率下降。BarcodeReader<Color32>这个泛型参数决定了Decode方法接收的像素数组类型,写错类型编译不过。
2.3 解码选项里真正影响成功率的几个参数
DecodingOptions是调优的主战场。0.16.8.0 里可调的项不少,但日常真正会动的就几个。
| 参数 | 默认值 | 作用 | 什么时候改 |
|---|---|---|---|
TryHarder | false | 尝试更多解码路径 | 图像质量差、识别不出时设为 true |
TryInverted | false | 同时尝试反色条码 | 遇到白条码黑底时设为 true |
PossibleFormats | 全部 | 限定条码类型 | 已知只扫 QR 时限定,能提速 |
CharacterSet | null | 指定字符编码 | 中文内容乱码时设为 "UTF-8" |
PureBarcode | false | 图像是纯条码无背景 | 裁剪后的干净图设为 true 可提速 |
TryHarder是最常被滥用的一个。设成 true 后解码器会做更多旋转和采样尝试,代价是单次解码耗时可能翻几倍。在手持设备上实时扫的场景,我一般先保持 false,只有在识别率不达标时才开,并且配合限定PossibleFormats把耗时压回来。CharacterSet这个坑后面单独讲,中文乱码十有八九是它没设。
3. 从一张 Texture2D 到解码结果:完整落地流程
3.1 摄像头帧转像素数组
Unity 里摄像头数据通常来自WebCamTexture。它不是Texture2D,不能直接GetPixels32,得先转一手。
// 假设 webCamTexture 已经 Play 并且有有效帧 // 创建一个和摄像头分辨率一致的 Texture2D 作为中转 var frameTexture = new Texture2D( webCamTexture.width, webCamTexture.height, TextureFormat.RGBA32, false ); // 把摄像头当前帧拷进中转纹理 frameTexture.SetPixels32(webCamTexture.GetPixels32()); frameTexture.Apply(); // 取出像素数组,交给解码器 Color32[] pixels = frameTexture.GetPixels32();TextureFormat.RGBA32要和GetPixels32返回的格式对应,用别的格式可能导致通道错位。Apply()不能省,否则 GPU 上的数据没同步回 CPU,GetPixels32拿到的是旧帧。这段代码每帧都new Texture2D会产生 GC 压力,实际项目里应该把frameTexture提到循环外复用,只在分辨率变化时重建。
3.2 调用 Decode 并处理结果
拿到Color32[]之后,解码就是一次方法调用。
// 配置解码选项 var options = new DecodingOptions { TryHarder = true, // 图像质量一般,开启深度尝试 PossibleFormats = new List<BarcodeFormat> { BarcodeFormat.QR_CODE, // 只扫二维码,减少无效尝试 BarcodeFormat.CODE_128 // 同时支持 Code128 一维码 }, CharacterSet = "UTF-8" // 中文内容必须指定 }; // 执行解码,width 和 height 必须和 pixels 数组匹配 var result = reader.Decode(pixels, frameTexture.width, frameTexture.height, options); if (result != null) { // result.Text 是解码出的字符串 // result.BarcodeFormat 告诉你扫到的是哪种码 Debug.Log($"识别成功:{result.Text},格式:{result.BarcodeFormat}"); } else { // 返回 null 不代表出错,只是这一帧没找到可识别的条码 // 实时扫描场景下直接跳过,等下一帧即可 }Decode的四个参数里,width和height必须和pixels数组的实际维度一致。传错会导致解码器按错误的行宽去读,结果要么是 null,要么是乱码。返回 null 是正常情况,摄像头对着空白区域时每帧都会返回 null,不要把它当异常处理,更不要在 null 时弹错误提示。
3.3 编码:把字符串生成条码图片
除了扫,0.16.8.0 也能生成条码。桌面端用BarcodeWriter配合System.Drawing。
using ZXing; using ZXing.Common; using ZXing.QrCode; using System.Drawing; // 创建二维码写入器 var writer = new BarcodeWriter<Bitmap> { Format = BarcodeFormat.QR_CODE, Options = new QrCodeEncodingOptions { Width = 300, // 输出图片宽度 Height = 300, // 输出图片高度 Margin = 1, // 静默区宽度,单位是模块数 CharacterSet = "UTF-8", // 内容含中文时必须设 ErrorCorrection = ZXing.QrCode.Internal.ErrorCorrectionLevel.M }, Renderer = new ZXing.Rendering.BitmapRenderer() }; // 生成位图 Bitmap qrImage = writer.Write("要编码的内容"); qrImage.Save("output.png");Margin是二维码四周的空白边,标准要求至少 4 个模块宽,设太小会导致某些扫描器识别困难。ErrorCorrectionLevel从 L 到 H 容错能力递增,但容错越高,同样尺寸下能存的数据越少。M 级别是日常用的平衡点。BitmapRenderer是桌面端的渲染器,Unity 里没有System.Drawing,得换成自定义的Color32Renderer,把输出渲染成Color32[]再塞进Texture2D。
4. 避坑与排查:那些让我返工的细节
4.1 中文内容解码乱码
现象:扫一个内容含中文的二维码,result.Text出来是一串问号或乱码。
原因:ZXing 解码时默认按 ISO-8859-1 去解释字节流,中文是多字节 UTF-8,被拆错了。
解决:在DecodingOptions里显式设CharacterSet = "UTF-8"。注意编码和解码两端都要设,生成二维码时QrCodeEncodingOptions.CharacterSet也要设成 UTF-8,否则源头就错了。
4.2 Unity 里报 TypeLoadException
现象:dll 放进 Plugins 后,运行时报TypeLoadException或MissingMethodException。
原因:dll 的目标框架和 Unity 脚本运行时版本不匹配。比如 Unity 设的是 .NET Standard 2.0,却放了net45的 dll。
解决:在 Unity 的 Player Settings 里确认Api Compatibility Level,然后从 NuGet 包里挑对应目录的 dll。0.16.8.0 的包解压后lib下会有多个框架目录,选错是高频翻车点。
4.3 实时扫描卡顿掉帧
现象:摄像头预览流畅,但一开解码就掉到十几帧。
原因:每帧都在做全分辨率解码,且TryHarder开着,单帧解码耗时超过帧间隔。
解决:三个方向。一是限定PossibleFormats,只留实际用到的码型。二是降采样,把摄像头帧缩到一半分辨率再解码,条码不需要全分辨率。三是控制解码频率,不必每帧都解,隔两三帧解一次,人眼几乎无感。
4.4 反色条码识别不出
现象:正常的黑白二维码能扫,白条码黑底的扫不出来。
原因:默认解码器只按「深色模块、浅色背景」的假设去找。
解决:设TryInverted = true。代价是解码耗时增加,所以只在确实存在反色条码的场景开,别默认打开。
4.5 生成的二维码扫不出来
现象:自己生成的二维码,用手机扫提示无法识别。
原因:Margin设成了 0,静默区被裁掉,扫描器找不到码的边界。
解决:Margin至少设 1,标准建议 4。另外检查Width和Height是否够大,内容多的时候尺寸太小会导致模块挤在一起。
5. 进阶:把解码耗时压到可预测的范围
实时扫描场景里,解码耗时的波动比平均值更值得关注。平均值 20ms 但偶尔飙到 200ms,体验上就是周期性卡顿。我的做法是给解码加一层时间预算控制。
using System.Diagnostics; // 记录上一帧解码耗时,用于动态调整策略 private long lastDecodeMs = 0; private int frameSkipCounter = 0; void TryDecode(Color32[] pixels, int width, int height) { // 如果上次解码超过 50ms,跳过接下来两帧,给主线程喘息 if (lastDecodeMs > 50 && frameSkipCounter < 2) { frameSkipCounter++; return; } frameSkipCounter = 0; var sw = Stopwatch.StartNew(); var result = reader.Decode(pixels, width, height, options); sw.Stop(); lastDecodeMs = sw.ElapsedMilliseconds; if (result != null) { OnBarcodeDetected(result.Text); } }这段逻辑的核心是:用上一帧的实测耗时决定这一帧要不要解。Stopwatch比DateTime精度高,适合毫秒级测量。阈值 50ms 和跳过帧数 2 是经验值,具体项目按目标帧率调。如果目标 30 帧,单帧预算 33ms,解码超过这个数就该让路。
另一个技巧是分区解码。手持设备扫货架时,条码通常出现在画面中央区域。与其每帧解全图,不如只取中央 60% 的区域去解,像素量降到三分之一,耗时立竿见影地降。代价是条码在边缘时扫不到,需要配合 UI 引导用户把码对准取景框。
验证解码稳定性,我一般会准备一组测试图:正常光照、强反光、倾斜 30 度、部分遮挡、低对比度各若干张,跑批量解码统计成功率。这个测试集不用大,二十张就能暴露大部分参数问题。每次改完DecodingOptions都跑一遍,比在真机上反复试快得多。
从那以后我每次在 Unity 里接 ZXing.Net,都强制先跑一遍这组测试图再上真机,省得在仓库里对着货架反复调参。希望帮到你。
本文还有配套的精品资源,点击获取