Unity有个项目使用ZXing.Net扫二维码时发现在昏暗环境下识别效果并不好,达不到微信扫一扫那种效果,于是选用了微信在OpenCV开源的二维码识别库(WeChatQRCode库),效果确实提升了不少。
关键步骤:
1.下载导入OpenCV for Unity插件 (可找陶某)
2.下载 “4个模型文件” 放StreamingAssets文件夹
3.实现模型文件拷贝到 “持久化文件夹”
由于我的项目是安卓端,构造WeChatQRCode对象需要提供这4个模型文件路径,一开始是直接提供StreamingAssets路径,奈何在安卓端无法识别,于是就在代码中就将4个模型文件拷贝到持久化文件夹路径下,再提供之后路径即可构造WeChatQRCode对象。下面是相关代码:
using System.Collections;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEngine.Networking;
public class QRCodeFilePath : MonoBehaviour
{
private string[] fourFileName = new string[4] { "detect.prototxt", "detect.caffemodel", "sr.prototxt", "sr.caffemodel" };
public static string[] fourFilePath { get; set; } = new string[4];
public static bool isLoaded { get; set; } = false;
private IEnumerator Start()
{
yield return null;
//判断是否已经存在4个文件
bool isHaveFiles = fourFileName.All((item) =>
{
return File.Exists(Application.persistentDataPath + "/" + item);
});
if (isHaveFiles)
{
fourFilePath = fourFileName.Select((item) => Application.persistentDataPath + "/" + item).ToArray();
isLoaded = true;
yield break;
}
//不存在,就将StreamingAssets下的4个文件拷贝到 持久化文件夹
for (int i = 0; i < fourFileName.Length; i++)
{
yield return CopyStePathFileToPerPath(fourFileName[i]);
}
isLoaded = true;
}
//实现一个将StreamingAssets文件夹下的某文件 拷贝到持久化文件夹下
private IEnumerator CopyStePathFileToPerPath(string fileName)
{
UnityWebRequest request = UnityWebRequest.Get(Application.streamingAssetsPath + "/" + fileName);
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
File.WriteAllBytes(Application.persistentDataPath + "/" + fileName, request.downloadHandler.data);
}
}
}
4.扫码线程对象实现
在Unity主线程解析二维码容易造成卡顿,需要另开一个线程。
using OpenCVForUnity.CoreModule;
using OpenCVForUnity.Wechat_qrcodeModule;
using System.Collections.Generic;
using System.Threading;
public static class QRScaner
{
private static Thread thread = null;//扫码线程(主线程扫码会造成卡顿)
private static WeChatQRCode weChatQRCode;//主对象
public static Mat mat;//传入要扫描的图片(mat)
public static List<string> rStrs = new List<string>();//返回的结果(多码支持)
public static List<Mat> rPoints = new List<Mat>();//返回的二维码的图像位置(多码支持)
public static void Start()
{
thread = new Thread(ThreadFun);
thread.Start();
}
public static void Stop()
{
if (thread != null)
{
thread.Abort();
}
}
private static void ThreadFun()
{
while (true)
{
if (mat != null)
{
if (weChatQRCode != null)
{
rStrs = weChatQRCode.detectAndDecode(mat, rPoints);
}
else
{
if (QRCodeFilePath.isLoaded)
{
var fourPath = QRCodeFilePath.fourFilePath;
weChatQRCode = new WeChatQRCode(fourPath[0], fourPath[1], fourPath[2], fourPath[3]);
}
}
mat = null;
}
}
}
}
5.扫码测试代码
using OpenCVForUnity.CoreModule;
using OpenCVForUnity.UnityUtils;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
public class WebCamMgr : MonoBehaviour
{
[SerializeField] RawImage rawImage;
private WebCamTexture camTexture;//相机贴图
private float checkQRTime = 1f;//每隔1秒检测一次二维码
private float checkQRDeltaTime;//检测二维码累计时间
private Mat mat;//检测的图像
private bool isFindQCode = true;//是否检测二维码
[SerializeField] GameObject pointPrefab;//显示二维码所在点位的预制体
#region
[SerializeField] GameObject showInfoPanel;//显示二维码扫描信息的Panel
private Text showInfoText;//显示信息的Text
#endregion
void Start()
{
#region 启动相机
var camName = "";
#if UNITY_EDITOR
camName = WebCamTexture.devices[0].name;
#else
camName = WebCamTexture.devices.FirstOrDefault((e) => e.isFrontFacing == false).name;//后摄像头
#endif
camTexture = new WebCamTexture(camName, 1920, 1080);
camTexture.Play();
rawImage.texture = camTexture;
(rawImage.transform as RectTransform).sizeDelta = new Vector2(camTexture.requestedWidth, camTexture.requestedHeight);
#if !UNITY_EDITOR
rawImage.transform.Rotate(Vector3.forward, -90);//由于安卓相机输出的是横向矩形贴图?,需旋转为纵向矩形贴图
#endif
#endregion
#region 启动二维码扫描线程
mat = new Mat(this.camTexture.height, this.camTexture.width, CvType.CV_8UC4);
QRScaner.Start();
#endregion
#region 显示扫描信息的面板
showInfoText = showInfoPanel.GetComponentInChildren<Text>(true);
//关闭按钮
showInfoPanel.GetComponentInChildren<Button>(true).onClick.AddListener(() =>
{
showInfoPanel.SetActive(false);
//摧毁所有点位
while (rawImage.transform.childCount != 0)
{
DestroyImmediate(rawImage.transform.GetChild(0).gameObject);
}
//相机播放
this.camTexture.Play();
//继续扫码
isFindQCode = true;
});
#endregion
}
private void Update()
{
if (!isFindQCode)
{
return;
}
checkQRDeltaTime += Time.deltaTime;
if (checkQRDeltaTime > checkQRTime)
{
checkQRDeltaTime = 0;
Utils.webCamTextureToMat(camTexture, mat);
QRScaner.mat = mat;
}
if (QRScaner.rStrs != null && QRScaner.rStrs.Count != 0)
{
isFindQCode = false;
camTexture.Pause();
List<string> rStrs = QRScaner.rStrs;//内容列表
List<Mat> rPoints = QRScaner.rPoints;//二维码点位列表
QRScaner.rStrs = null;
//显示二维码的位置
for (int i = 0; i < rStrs.Count; i++)
{
GameObject tempObj = Instantiate(pointPrefab, rawImage.transform);
tempObj.transform.localPosition = ConvertToCenterOrigin(camTexture.width, camTexture.height, GetMatV2(rPoints[i]));
}
//等待一小段时间,显示扫描结果面板
IEnumerator WaitShowPanel()
{
yield return new WaitForSeconds(.5f);
showInfoPanel.SetActive(true);
StringBuilder stringBuilder = new StringBuilder("扫描结果:");
foreach (var item in rStrs)
{
stringBuilder.AppendLine(item);
}
showInfoText.text = stringBuilder.ToString();
showInfoPanel.SetActive(true);
}
StartCoroutine(WaitShowPanel());
}
}
//二维码Mat转换为二维坐标
public Vector2 GetMatV2(Mat mat)
{
Vector2 point0 = new Vector2((int)mat.get(0, 0)[0], (int)mat.get(0, 1)[0]);
Vector2 point1 = new Vector2((int)mat.get(1, 0)[0], (int)mat.get(1, 1)[0]);
Vector2 point2 = new Vector2((int)mat.get(2, 0)[0], (int)mat.get(2, 1)[0]);
Vector2 point3 = new Vector2((int)mat.get(3, 0)[0], (int)mat.get(3, 1)[0]);
float centerX = (int)((point0.x + point1.x + point2.x + point3.x) / 4.0f);
float centerY = (int)((point0.y + point1.y + point2.y + point3.y) / 4.0f);
return new Vector2(centerX, centerY);
}
/// <summary>
/// 将基于矩形左下角为原点的坐标转换为以矩形中心为原点的坐标
/// </summary>
/// <param name="rectWidth">矩形的宽度</param>
/// <param name="rectHeight">矩形的高度</param>
/// <param name="point">基于左下角为原点的Vector2坐标</param>
/// <returns>基于矩形中心为原点的Vector2坐标</returns>
public Vector2 ConvertToCenterOrigin(float rectWidth, float rectHeight, Vector2 point)
{
float x = point.x;
float y = point.y;
// 计算矩形中心点相对于左下角的偏移量
float centerXOffset = rectWidth / 2;
float centerYOffset = rectHeight / 2;
// 将基于左下角的坐标转换为基于中心的坐标
float newX = x - centerXOffset;
float newY = centerYOffset - y;
return new Vector2(newX, newY);
}
}
二维码提示点位由于线程时间不同步导致有些偏移,如果你有好的优化方法还请多指教