Webassembly初探:在浏览器运行ZXing.Net WASM进行条形码识别

344 阅读4分钟

前言

最近遇到个需求,要求是在前端实现一张图片里面识别多个二维码。需要在本地进行识别,尝试了一些前端条形码识别库ZXing-js等,结果都不太理想,不支持在一张图片里面同时识别多条形码。因为后端技术栈使用的是.NET,所以找了ZXing.NET这个识别库,这个库还可以支持一张图片里面多个条形码的识别。所以想到了使用这个库编译成webassembly在前端进行本地调用识别。

后续可以配合opencv先定位条形码,再裁剪、预处理,最后进行识别,可以有更高的识别度。后面会出一篇文章来进一步提高识别率

前置准备

  • Dotnet SDK
  • Node

步骤

安装wasm-tools

使用Dotnet命令行安装wasm-tools

dotnet workload install wasm-tools

或者在VS中安装.NET WebAssembly Build Tools组件 image.png

创建项目

初始化项目

dotnet new console

使用dotnet new console创建一个空项目 创建完之后我们用VS或者VS code打开

image.png

添加所需要的依赖

//csproj项目配置文件
 <Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>wasm_demo</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <WasmMainJSPath>main.js</WasmMainJSPath>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
    <PackageReference Include="SkiaSharp" Version="2.88.8" />
    <PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.8" />
    <PackageReference Include="SkiaSharp.Views.Blazor" Version="2.88.8" />
    <PackageReference Include="ZXing.Net" Version="0.16.9" />
    <PackageReference Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.14" />
  </ItemGroup>

</Project>

根据个人需要添加依赖,注意有的包并不支持WASM

代码编写

添加代码,在代码中我添加了两个方法,一个是Base64ToBitmap,用于将前端JS传输过来的Base64图片转换成BitMap。另一个是DecodeImage,该方法是将得到的BitMap传递给Zxing.Net进行条形码识别。

//Program.cs

using System.Runtime.InteropServices.JavaScript;
using Newtonsoft.Json;
using SkiaSharp;
using ZXing;

public partial class JSBridge
{

    [JSImport("globalThis.console.log")]
    internal static partial void Log([JSMarshalAs<JSType.String>] string message);



    public static SKBitmap Base64ToBitmap(string base64String)
    {
        // 将 Base64 字符串解码为字节数组
        byte[] imageBytes = Convert.FromBase64String(base64String);
        SKMemoryStream stream = new SKMemoryStream(imageBytes);
        return SKBitmap.Decode(stream);
    }


    [JSExport]
    internal static string DecodeImage(string imgbase64)
    {

        // 读取图像文件
        var barcodeBitmap = Base64ToBitmap(imgbase64);

        List<string> resList = new List<string>();
        var barcodeReader = new ZXing.SkiaSharp.BarcodeReader
        {
            AutoRotate = true,
            Options = new ZXing.Common.DecodingOptions
            {
                PossibleFormats = new[] { BarcodeFormat.CODE_128 },
                TryHarder = true,
                TryInverted = true,
                PureBarcode = false
            }
        };//参数项根据具体项目需求进行调整以提高识别率,在当前需求中条形码就只有CODE128格式的,所以就只添加了CODE128

        // 解码图像中的条形码
        var barcodeResults = barcodeReader.DecodeMultiple(barcodeBitmap);
        // 如果成功识别到条形码
        if (barcodeResults != null && barcodeResults.Length > 0)
        {

            Log($"Found {barcodeResults.Length} barcodes:");
            for (int i = 0; i < barcodeResults.Length; i++)
            {


                Log($"  Text: {barcodeResults[i].Text}");
                resList.Add(barcodeResults[i].Text);
                if (barcodeResults[i].ResultPoints.Length > 0)
                {
                    Log($"  Position: ({barcodeResults[i].ResultPoints[0].X}, {barcodeResults[i].ResultPoints[0].Y})");
                }
            }
        }
        else
        {
            Console.WriteLine("No CODE128 barcodes found in the image.");
        }
        var resJson = JsonConvert.SerializeObject(resList);


        //!important 记得要进行垃圾回收,不然在浏览器端会一直占用内存
        GC.Collect();
        return resJson;
    }


}

public class Program
{

    [STAThread]
    static void Main(params string[] paramaters)
    {
        Console.WriteLine("WASM MAIN");
    }
}

在Program.cs同级目录中创建main.js文件,用于调用JSBridge中的方法

//main.js
import { dotnet } from "./_framework/dotnet.js";
let exportsPromise = null;

const createRuntimeAndGetExports = async () => {
  const { getAssemblyExports, getConfig } = await dotnet.create();
  const config = getConfig();
  return await getAssemblyExports(config.mainAssemblyName);
};


export async function decodeImage(url) {
  if (!exportsPromise) {
    exportsPromise = createRuntimeAndGetExports();
  }
  const exports = await exportsPromise;
  return exports.JSBridge.DecodeImage(url);
}


构建WASM

添加完代码之后,我们可以执行 dotnet publish -c Release将dotnet代码构建成WASM,执行完成之后,可以在bin/Release/net8.0/browser-wasm目录中看到AppBundle文件夹。这个文件夹就是我们需要放在前端里面的文件夹。 将AppBundle文件夹复制到前端项目中,在这里我放到了vue项目中的src/assets/wasm文件夹中

image.png

调用WASM

在这里的前端项目是Vite+Vue3,我写了一个简单的demo,demo包含图片上传和识别按钮。我们直接在按钮按钮里面调用该wasm进行识别。

<script setup>
import { ref } from "vue";
import * as wasm from './assets/wasm/main'

const imgSrc = ref("");
const decodeRes = ref();
const handleFileChange = (e) => {
  const file = e.target.files[0];
  const reader = new FileReader();
  reader.onload = () => {
    imgSrc.value = reader.result;
  };
  reader.readAsDataURL(file);
};
const handleClick = async () => {
  console.log(imgSrc.value);
  const res = await wasm.decodeImage(imgSrc.value.split(',')[1])
   decodeRes.value = JSON.parse(res)

};
</script>

<template>
  <div>
    <input type="file" @change="handleFileChange" />
    <button @click="handleClick">decode</button>
    <img :src="imgSrc" class="preview" />
    <div v-if="decodeRes&&decodeRes.length==0">没有识别到条形码</div>
    <div v-for="item,index in decodeRes">
      <div>结果{{ index+1 }}:{{ item }}</div>
    </div>
  </div>
</template>

<style scoped>

.preview{
  width:50%;
}

</style>

通过上传条形码图片之后,我们点击识别按钮,可以看到已经识别到了结果

image.png

缺点

  • 识别率不算高,适用于简单的图片,能够处理。对于复杂图片最好还是走后端、云服务API、机器学习上模型。
  • 使用浏览器用户的CPU进行处理,依靠CPU单核性能。
  • 会吃不少内存,如下面是识别前和识别后的内存占用,所以需要做好wasm中内存的释放。浏览器V8引擎垃圾回收不会自动处理这部分的内存占用。

image.png

总结

通过前端执行wasm进行条形码识别的方式,可以不用通过后端进行识别,如果图片较大的话,直接在前端进行识别,能够节省不少网络资源。 小白wasm初探,对很多wasm的东西并不了解,大佬们有更好的见解可以分享一下。

github仓库地址:StoryKing123/wasm-demo

引用

Incorporating .NET Functionality into JavaScript using WebAssembly