using System;
using System.Collections;
using System.Collections.Generic;
using Kill.UI.Components;
using UnityEngine;
using UnityEngine.UI;
namespace Kill.UI.Pages
{
///
/// 二维码扫描器 - 调用摄像头扫描二维码
///
public class ScanQRcode : MonoBehaviour
{
[Serializable]
public enum ScanType
{
ConnectDevice, // 连接设备
Other // 其他用途
}
[Header("UI组件")]
public RawImage cameraPreview; // 摄像头预览
[Header("扫描设置")]
float scanInterval = 0.2f; // 扫描间隔(秒)
public ScanType scanType = ScanType.ConnectDevice; // 扫描类型
[Header("小二维码优化")]
[Tooltip("启用图像放大(推荐用于小二维码)")]
public bool enableUpscale = true;
[Tooltip("放大倍数")]
public float upscaleFactor = 2f;
[Header("性能优化")]
[Tooltip("降低扫描分辨率(推荐值:640或480)")]
public int scanResolutionWidth = 1080;
[Tooltip("扫描处理线程数")]
public bool useAsyncScan = true;
[Header("自动对焦")]
[Tooltip("启用自动对焦")]
public bool enableAutoFocus = true;
[Tooltip("对焦间隔(秒)")]
public float focusInterval = 1f;
// 摄像头相关
private WebCamTexture webCamTexture;
private WebCamDevice currentDevice;
private bool isScanning = false;
private bool isProcessing = false;
// 扫描结果回调
public event Action OnQRCodeScanned;
void OnEnable()
{
StartScan();
}
void OnDisable()
{
StopScan();
}
///
/// 开始扫描
///
public void StartScan()
{
if (isScanning) return;
StartCoroutine(InitializeCamera());
}
///
/// 停止扫描
///
public void StopScan()
{
isScanning = false;
StopAllCoroutines();
if (webCamTexture != null && webCamTexture.isPlaying)
{
webCamTexture.Stop();
webCamTexture = null;
}
// 清理预览纹理
if (previewTexture != null)
{
Destroy(previewTexture);
previewTexture = null;
}
}
///
/// 关闭扫码界面
///
public void Close()
{
StopScan();
gameObject.SetActive(false);
}
///
/// 初始化摄像头
///
private IEnumerator InitializeCamera()
{
// Android 使用 Permission API 请求权限
#if UNITY_ANDROID && !UNITY_EDITOR
if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera))
{
UnityEngine.Android.Permission.RequestUserPermission(UnityEngine.Android.Permission.Camera);
// 等待权限请求结果
yield return new WaitUntil(() => UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera));
}
if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera))
{
UpdateStatus("100085");
yield break;
}
#else
// 其他平台使用传统方式
yield return Application.RequestUserAuthorization(UserAuthorization.WebCam);
if (!Application.HasUserAuthorization(UserAuthorization.WebCam))
{
UpdateStatus("100085");
yield break;
}
#endif
// 获取后置摄像头
WebCamDevice[] devices = WebCamTexture.devices;
if (devices.Length == 0)
{
UpdateStatus("100086");
yield break;
}
// 优先使用后置摄像头,并选择支持自动对焦的摄像头
string deviceName = devices[0].name;
for (int i = 0; i < devices.Length; i++)
{
if (!devices[i].isFrontFacing)
{
deviceName = devices[i].name;
currentDevice = devices[i];
break;
}
}
RectTransform cameraPreviewRect=cameraPreview.GetComponent();
// 使用较低的扫描分辨率以提高性能
int targetWidth = 2160;
// 创建摄像头纹理
webCamTexture = new WebCamTexture(deviceName, targetWidth, 30);
// 开始播放
webCamTexture.Play();
// 等待摄像头启动
yield return new WaitUntil(() => webCamTexture.width > 100);
webCamTexture.autoFocusPoint=new Vector2(0.5f,0.5f);
// 调整预览画面比例
AdjustPreviewAspect();
isScanning = true;
// 开始预览更新和扫描
StartCoroutine(PreviewUpdateCoroutine());
StartCoroutine(ScanCoroutine());
}
///
/// 调整预览画面比例 - 从相机画面中心截取最大适合预览画面比例的画面
/// 手机竖着拿,摄像头默认横向输出,需要旋转并截取中间区域
/// 预览框是横向的(1080x680),显示旋转后的竖向画面
///
private void AdjustPreviewAspect()
{
if (webCamTexture == null || cameraPreview == null) return;
// 获取相机分辨率(摄像头默认横向,如 1920x1080)
int cameraWidth = webCamTexture.width; // 1920
int cameraHeight = webCamTexture.height; // 1080
// 获取预览区域尺寸(横向预览框,比如 1080x680)
RectTransform previewRect = cameraPreview.GetComponent();
int previewWidth = (int)previewRect.rect.width; // 1080
int previewHeight = (int)previewRect.rect.height; // 680
// 创建预览纹理(与预览框相同尺寸)
if (previewTexture == null || previewTexture.width != previewWidth || previewTexture.height != previewHeight)
{
if (previewTexture != null)
{
Destroy(previewTexture);
}
previewTexture = new Texture2D(previewWidth, previewHeight, TextureFormat.RGB24, false);
}
// 从相机画面中裁切并旋转
// 手机竖着拿,相机横向输出,需要顺时针旋转90度
// 从相机画面中间截取适合预览比例的竖向区域
CutAndRotateCameraFrame(cameraWidth, cameraHeight, previewWidth, previewHeight);
// 设置预览纹理
cameraPreview.texture = previewTexture;
cameraPreview.uvRect = new Rect(0, 0, 1, 1);
// 重置旋转(因为已经在像素层面旋转了)
previewRect.localEulerAngles = Vector3.zero;
Debug.Log($"[ScanQRcode] 预览调整 - 相机: {cameraWidth}x{cameraHeight}, 预览: {previewWidth}x{previewHeight}");
}
private Texture2D previewTexture;
///
/// 从相机画面裁切并旋转,生成预览画面
///
private void CutAndRotateCameraFrame(int camW, int camH, int targetW, int targetH)
{
// 相机横向 1920x1080,需要顺时针旋转90度变成 1080x1920
// 预览框横向 1080x680
// 从旋转后的 1080x1920 中截取中间 1080x680 区域
// 计算裁切区域(在原始相机画面上的坐标)
// 旋转后高度是 camW(1920),需要截取 targetH(680) 高度
// 从中间截取:起始Y = (1920 - 680) / 2 = 620
int cropY = (camW - targetH) / 2; // 620
// 获取相机像素数据
Color32[] cameraPixels = webCamTexture.GetPixels32();
Color32[] targetPixels = new Color32[targetW * targetH];
// 顺时针旋转90度并裁切
// 原始坐标 (x, y) -> 顺时针旋转90度后 (y, camW-1-x)
// 目标 (x, y) 对应源:
// srcX = cropY + y (在裁切区域内纵向移动)
// srcY = x (横向直接对应,不翻转)
for (int y = 0; y < targetH; y++)
{
for (int x = 0; x < targetW; x++)
{
// 目标像素在预览图中的位置 (x, y)
// 对应原始相机中的位置
int srcX = cropY + (targetH - 1 - y); // 从裁切区域底部开始,向上取像素(修复上下颠倒)
int srcY = x; // 横向直接对应
if (srcX >= 0 && srcX < camW && srcY >= 0 && srcY < camH)
{
int srcIndex = srcY * camW + srcX;
int dstIndex = y * targetW + x;
targetPixels[dstIndex] = cameraPixels[srcIndex];
}
}
}
// 设置像素到预览纹理
previewTexture.SetPixels32(targetPixels);
previewTexture.Apply();
}
[SerializeField] private float previewUpdateInterval = 0.03f; // 预览更新间隔(20fps)
///
/// 预览更新协程 - 降低更新频率避免卡顿
///
private IEnumerator PreviewUpdateCoroutine()
{
while (isScanning && webCamTexture != null && webCamTexture.isPlaying)
{
// 更新预览画面(限制频率)
if (previewTexture != null)
{
CutAndRotateCameraFrame(webCamTexture.width, webCamTexture.height, previewTexture.width, previewTexture.height);
}
yield return new WaitForSeconds(previewUpdateInterval);
}
}
///
/// 扫描协程
///
private IEnumerator ScanCoroutine()
{
// 等待一帧确保摄像头已准备好
yield return null;
while (isScanning && webCamTexture != null && webCamTexture.isPlaying)
{
// 避免同时处理多帧
if (isProcessing)
{
yield return null;
continue;
}
isProcessing = true;
// 使用预览纹理进行扫描
if (previewTexture != null)
{
// 异步解析二维码以避免卡顿
if (useAsyncScan)
{
string result = null;
bool scanComplete = false;
// 在后台线程执行解码
System.Threading.ThreadPool.QueueUserWorkItem(_ =>
{
try
{
result = DecodeQRCode(previewTexture);
}
catch (Exception e)
{
Debug.LogError($"[ScanQRcode] 解码异常: {e.Message}");
}
finally
{
scanComplete = true;
}
});
// 等待解码完成
yield return new WaitUntil(() => scanComplete);
// 处理结果
if (!string.IsNullOrEmpty(result))
{
ProcessScanResult(result);
}
}
else
{
// 同步解码
string result = DecodeQRCode(previewTexture);
if (!string.IsNullOrEmpty(result))
{
ProcessScanResult(result);
}
}
}
isProcessing = false;
yield return new WaitForSeconds(scanInterval);
}
}
///
/// 处理扫描结果
///
private void ProcessScanResult(string result)
{
Debug.Log($"扫描结果: {result}");
// 根据扫描类型处理结果
if (scanType == ScanType.ConnectDevice)
{
// 从结果中提取 MAC 地址
string macAddress = ExtractMacAddress(result);
if (!string.IsNullOrEmpty(macAddress))
{
// 扫描成功,仅回传 MAC 地址
OnQRCodeScanned?.Invoke(macAddress);
OnQRCodeScanned-=OnQRCodeScanned;
}
else
{
// 二维码不符合要求,显示提示
UpdateStatus("100084");
}
}
else
{
if (!string.IsNullOrEmpty(result))
{
// 其他用途,直接回传结果
OnQRCodeScanned?.Invoke(result);
}
else
{
// 二维码不符合要求,显示提示
UpdateStatus("100084");
}
}
}
///
/// 获取摄像头当前帧
///
private Texture2D GetSnapshot()
{
if (webCamTexture == null || !webCamTexture.isPlaying) return null;
try
{
Texture2D snapshot = new Texture2D(webCamTexture.width, webCamTexture.height);
snapshot.SetPixels(webCamTexture.GetPixels());
snapshot.Apply();
// // 如果启用放大,对图像进行放大处理
// if (enableUpscale && upscaleFactor > 1f)
// {
// snapshot = UpscaleTexture(snapshot, upscaleFactor);
// }
return snapshot;
}
catch
{
return null;
}
}
///
/// 放大纹理(使用双线性插值)- 优化版本
///
private Texture2D UpscaleTexture(Texture2D source, float factor)
{
// 限制放大倍数,避免过度消耗性能
factor = Mathf.Clamp(factor, 1f, 3f);
int newWidth = Mathf.RoundToInt(source.width * factor);
int newHeight = Mathf.RoundToInt(source.height * factor);
// 限制最大尺寸,避免内存问题
newWidth = Mathf.Min(newWidth, 1280);
newHeight = Mathf.Min(newHeight, 720);
// 如果尺寸没有变化,直接返回原图
if (newWidth <= source.width || newHeight <= source.height)
{
return source;
}
Texture2D result = new Texture2D(newWidth, newHeight, TextureFormat.RGB24, false);
// 使用 RenderTexture 进行高质量缩放
RenderTexture rt = RenderTexture.GetTemporary(newWidth, newHeight, 0, RenderTextureFormat.RGB565);
RenderTexture.active = rt;
Graphics.Blit(source, rt);
result.ReadPixels(new Rect(0, 0, newWidth, newHeight), 0, 0);
result.Apply();
RenderTexture.active = null;
RenderTexture.ReleaseTemporary(rt);
// 销毁原始纹理
Destroy(source);
return result;
}
///
/// 解析二维码
///
private string DecodeQRCode(Texture2D texture)
{
try
{
// 全图扫描
string result = TryDecodeRegion(texture, 0, 0, texture.width, texture.height);
if (!string.IsNullOrEmpty(result))
return result;
return null;
}
catch (Exception ex)
{
Debug.LogError($"二维码解析失败: {ex.Message}");
return null;
}
}
///
/// 尝试解码指定区域
///
private string TryDecodeRegion(Texture2D texture, int startX, int startY, int width, int height)
{
try
{
// 确保区域在有效范围内
startX = Mathf.Max(0, startX);
startY = Mathf.Max(0, startY);
width = Mathf.Min(width, texture.width - startX);
height = Mathf.Min(height, texture.height - startY);
// 获取全图像素数据
Color32[] allPixels = texture.GetPixels32();
// 提取指定区域的像素
Color32[] regionPixels = new Color32[width * height];
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
int srcIndex = (startY + y) * texture.width + (startX + x);
int dstIndex = y * width + x;
if (srcIndex < allPixels.Length)
{
regionPixels[dstIndex] = allPixels[srcIndex];
}
}
}
// 转换为 RGB 字节数组(ZXing 内部会自动处理灰度转换)
byte[] rgbBytes = new byte[regionPixels.Length * 3];
for (int i = 0; i < regionPixels.Length; i++)
{
rgbBytes[i * 3] = regionPixels[i].r;
rgbBytes[i * 3 + 1] = regionPixels[i].g;
rgbBytes[i * 3 + 2] = regionPixels[i].b;
}
// 使用 RGBLuminanceSource,让 ZXing 内部处理灰度转换
var luminanceSource = new ZXing.RGBLuminanceSource(rgbBytes, width, height);
// 尝试解码
string result = TryDecodeWithBinarizer(luminanceSource);
if (!string.IsNullOrEmpty(result))
return result;
return null;
}
catch
{
return null;
}
}
///
/// 尝试使用不同的二值化算法解码
///
private string TryDecodeWithBinarizer(ZXing.LuminanceSource luminanceSource)
{
var hints = new Dictionary
{
{ ZXing.DecodeHintType.TRY_HARDER, true },
{ ZXing.DecodeHintType.POSSIBLE_FORMATS, new List { ZXing.BarcodeFormat.QR_CODE } }
};
var reader = new ZXing.QrCode.QRCodeReader();
// 尝试 HybridBinarizer
try
{
var binarizer = new ZXing.Common.HybridBinarizer(luminanceSource);
var binaryBitmap = new ZXing.BinaryBitmap(binarizer);
var result = reader.decode(binaryBitmap, hints);
if (result != null)
return result.Text;
}
catch { }
// 尝试 GlobalHistogramBinarizer
try
{
var binarizer = new ZXing.Common.GlobalHistogramBinarizer(luminanceSource);
var binaryBitmap = new ZXing.BinaryBitmap(binarizer);
var result = reader.decode(binaryBitmap, hints);
if (result != null)
return result.Text;
}
catch { }
return null;
}
///
/// 应用锐化滤镜(拉普拉斯算子)
/// 增强图像边缘,提高模糊二维码识别率
///
private byte[] ApplySharpenFilter(byte[] input, int width, int height)
{
byte[] output = new byte[input.Length];
// 拉普拉斯锐化核
// 0 -1 0
// -1 5 -1
// 0 -1 0
int[] kernel = { 0, -1, 0, -1, 5, -1, 0, -1, 0 };
int kernelSize = 3;
int halfKernel = kernelSize / 2;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
int sum = 0;
// 应用卷积核
for (int ky = -halfKernel; ky <= halfKernel; ky++)
{
for (int kx = -halfKernel; kx <= halfKernel; kx++)
{
int py = Mathf.Clamp(y + ky, 0, height - 1);
int px = Mathf.Clamp(x + kx, 0, width - 1);
int pixelIndex = py * width + px;
int kernelIndex = (ky + halfKernel) * kernelSize + (kx + halfKernel);
sum += input[pixelIndex] * kernel[kernelIndex];
}
}
// 限制在0-255范围内
output[y * width + x] = (byte)Mathf.Clamp(sum, 0, 255);
}
}
return output;
}
///
/// 从字符串中提取 MAC 地址
/// 支持格式:
/// 1. 带冒号格式:98:EA:A0:02:4E:06
/// 2. 不带冒号格式:98eaa002658e
/// 3. 从键值对格式提取:Name:xxx;MAC:98eaa002658e
///
private string ExtractMacAddress(string input)
{
if (string.IsNullOrEmpty(input))
return null;
string macAddress = null;
// 尝试从键值对格式提取 MAC:xxx
// 匹配 MAC: 后面跟着 12 位十六进制字符(可能带冒号或不带)
System.Text.RegularExpressions.Regex macKeyValueRegex =
new System.Text.RegularExpressions.Regex(@"MAC:([0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2})");
System.Text.RegularExpressions.Match kvMatch = macKeyValueRegex.Match(input);
if (kvMatch.Success)
{
macAddress = kvMatch.Groups[1].Value;
}
else
{
// 尝试匹配带冒号的标准 MAC 格式
System.Text.RegularExpressions.Regex macColonRegex =
new System.Text.RegularExpressions.Regex(@"[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}");
System.Text.RegularExpressions.Match colonMatch = macColonRegex.Match(input);
if (colonMatch.Success)
{
macAddress = colonMatch.Value;
}
else
{
// 尝试匹配不带冒号的 12 位十六进制字符
System.Text.RegularExpressions.Regex macNoColonRegex =
new System.Text.RegularExpressions.Regex(@"[0-9A-Fa-f]{12}");
System.Text.RegularExpressions.Match noColonMatch = macNoColonRegex.Match(input);
if (noColonMatch.Success)
{
macAddress = noColonMatch.Value;
}
}
}
if (!string.IsNullOrEmpty(macAddress))
{
// 统一转换为带冒号的大写格式
return FormatMacAddress(macAddress);
}
return null;
}
///
/// 将 MAC 地址统一格式化为带冒号的大写格式
/// 输入: 98eaa002658e 或 98:EA:A0:02:65:8E
/// 输出: 98:EA:A0:02:65:8E
///
private string FormatMacAddress(string mac)
{
if (string.IsNullOrEmpty(mac))
return null;
// 移除所有冒号
string cleanMac = mac.Replace(":", "").Replace("-", "").ToUpper();
// 检查长度是否为 12
if (cleanMac.Length != 12)
return null;
// 格式化为 XX:XX:XX:XX:XX:XX
return string.Format("{0}:{1}:{2}:{3}:{4}:{5}",
cleanMac.Substring(0, 2),
cleanMac.Substring(2, 2),
cleanMac.Substring(4, 2),
cleanMac.Substring(6, 2),
cleanMac.Substring(8, 2),
cleanMac.Substring(10, 2));
}
///
/// 更新状态文字
///
private void UpdateStatus(string code)
{
ToastUI.Show(code);
}
void OnDestroy()
{
StopScan();
gameObject.SetActive(false);
}
}
}