【Unity Sentis应用-手写数字识别】


前言

本文介绍的是使用Unity的AI工具Sentis,结合ONNX大模型,匹配手写数字的识别结果的简单应用。


一、导入Sentis Package

新建Unity工程,Windows-PackageManager,输入以下包地址
在这里插入图片描述

com.unity.sentis

如果下载失败,可能是没有内测资格,需要申请测试资格,点击下面链接跳转至申请界面
跳转链接

二、下载Onnx大模型

此处我们下载的是MNIST手写体识别大模型,下载链接

讲下载后的Onnx文件放到Unity的资源目录Assets下
在这里插入图片描述

三、手写数字生成图片

使用LineRender实现画笔功能

using UnityEngine;
using UnityEngine.Serialization;


//画笔
public class Paint : MonoBehaviour
{
    private GameObject _clone;
    private LineRenderer _line;
    private int _number;

    //带有LineRender物体
    [FormerlySerializedAs("Target")] [Header("LineRender预制体")]
    public GameObject target;

    [FormerlySerializedAs("PaintCamera")] public Camera paintCamera;

   
    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            //实例化对象
            _clone = Instantiate(target, target.transform.position, Quaternion.identity);
            //获得该物体上的LineRender组件
            _line = _clone.GetComponent<LineRenderer>();
            //设置起始和结束的颜色
            _line.startColor = Color.white;
            _line.endColor = Color.white;
            //设置起始和结束的宽度
            // _Line.SetWidth(0.2f, 0.1f);
            _line.startWidth = 0.2f;
            _line.endWidth = 0.1f;
            //计数
            _number = 0;
        }

        if (Input.GetMouseButton(0))
        {
            //每一帧检测,按下鼠标的时间越长,计数越多
            _number++;
            //设置顶点数
            //_Line.SetVertexCount(_Number);
            _line.positionCount = _number;
            var screen = Input.mousePosition;
            var pos = paintCamera.ScreenToWorldPoint(screen);
            pos.z = 5;
            //设置顶点位置(顶点的索引,将鼠标点击的屏幕坐标转换为世界坐标)
            _line.SetPosition(_number - 1, pos);
        }

        //清除绘画
        if (Input.GetMouseButtonDown(1))
        {
            GameObject[] _Draw = GameObject.FindGameObjectsWithTag("DrawLine");
            for (int i = 0; i < _Draw.Length; i++)
            {
                Destroy(_Draw[i]);
            }
        }
    }

    private void OnGUI()
    {
        GUI.Label(new Rect(10, 20, 1000, 500), "【绘制】拖动鼠标左键绘制数字\n【取消绘制】点击鼠标右键 \n【识别】Space \n");
    }
}

将画完的线条通过截图的方式生成一张图片

using UnityEngine;

public class CropPicture : MonoBehaviour
{
    public Camera cropCamera; //待截图的目标摄像机
    private RenderTexture _renderTexture;
    private Texture2D _texture2D;

    public HandwrittenDigitDiscern classifyHandwritten;
    private void Start()
    {
        _renderTexture = new RenderTexture(2048, 2048, 32);
        _texture2D = new Texture2D(2048, 2048, TextureFormat.ARGB32, false);
        cropCamera.targetTexture = _renderTexture;
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            RenderTexture.active = _renderTexture;
            _texture2D.ReadPixels(new Rect(0, 0, _renderTexture.width, _renderTexture.height), 0, 0);
            _texture2D.Apply();
            RenderTexture.active = null;

            classifyHandwritten.DiscernAndJudge(_texture2D);
        }
    }
}

四、手写体识别

此处使用的ModelAsset即为上述的手写体识别的大模型文件

using UnityEngine;
using Unity.Sentis;
using Unity.Sentis.Layers;

/// <summary>
/// 手写体数字 识别
/// </summary>
public class HandwrittenDigitDiscern : MonoBehaviour
{
    [Header("使用大模型")]
    public ModelAsset _ModelAsset;

    //加载模型
    Model _RuntimeModel;
    //工作流
    IWorker _Worker;

    [Header("返回 概率列表")]
    public float[] _Results;

   

    /// <summary>
    /// 识别于判断
    /// </summary>
    public void DiscernAndJudge(Texture2D _Texture2D)
    {
        // 创建运行时模型
        _RuntimeModel = ModelLoader.Load(_ModelAsset);

        // 将 softmax 层添加到模型的末尾,而不是非 softmax 输出
        string _SoftmaxOutputName = "Softmax_Output";
        _RuntimeModel.AddLayer(new Softmax(_SoftmaxOutputName, _RuntimeModel.outputs[0]));
        _RuntimeModel.outputs[0] = _SoftmaxOutputName;

        // 将输入数据创建为张量
        using Tensor _InputTensor = TextureConverter.ToTensor(_Texture2D, width: 28, height: 28, channels: 1);

        // 创建引擎
        //在 Sentis 中,工作线程是推理引擎。您可以创建一个工作线程以将模型分解为可执行任务,在 GPU 或 CPU 上运行任务,然后输出结果。
        //例如,创建一个使用 Sentis 计算着色器在 GPU 上运行的工作线程:
        _Worker = WorkerFactory.CreateWorker(BackendType.GPUCompute, _RuntimeModel);

        // 使用输入数据运行模型
        _Worker.Execute(_InputTensor);

        // 获得结果
        using TensorFloat _OutputTensor = _Worker.PeekOutput() as TensorFloat;

        // 在读取张量数据之前将其移动到CPU
        _OutputTensor.MakeReadable();

        //返回数据填充
        _Results = _OutputTensor.ToReadOnlyArray();

        Debug.Log("识别结果:"+ GetValue());
    }

    private int GetValue()
    {
        int index = 0;
        float value=0;
        for (int i = 0; i < _Results.Length; i++)
        {
            if (value< _Results[i])
            {
                value = _Results[i];
                index = i;
            }
        }
        return index;
    }

    /// <summary>
    /// 当前脚本处于 禁用或非活动时执行
    /// </summary>
    void OnDisable()
    {
        // 告诉GPU我们已经处理完引擎使用的内存了
        _Worker?.Dispose();
    }
}


五、手写体识别

在这里插入图片描述


总结

Unity Sentis是一款无缝集成自定义AI模型的工具,专为Unity引擎设计。该工具可以在Unity运行的各个平台上流畅运行,无需额外的优化。Unity Sentis允许大家将神经网络嵌入到他们的构建中,从而提供实时的体验,增强游戏玩法和功能在不同平台上的表现。

<think>我们正在处理用户关于Unity手写体'i'识别的需求。根据引用内容,我们可以借鉴以下思路:1.引用[1]提到使用Sentis进行手写数字识别,其中涉及将手写线条生成图片,然后进行识别。我们可以类似地处理字母'i'的识别。2.引用[2]提到使用Unity虚拟环境生成手势数据,我们可以考虑生成或收集手写字母'i'的数据用于训练。3.引用[3]提到使用OpenCV进行手写数字的训练与识别,包括预处理(二值化、分割、调整)和预测。我们可以采用类似方法处理字母'i'。4.引用[4]提到Unity中通过标签或名字查找对象,这可能在交互中用到,比如获取用户绘制的笔迹。实现步骤:1.捕获用户输入:在Unity中实现一个绘图板,让用户书写字母'i'。2.预处理:将用户绘制的笔迹转换为图片,并进行预处理(如二值化、大小归一化等)。3.模型选择:训练一个分类模型(如使用机器学习库或深度学习模型)来识别字母'i'。4.集成到Unity:将训练好的模型集成到Unity中,对预处理后的图片进行预测。具体方法:方法一:使用OpenCV(如引用[3])-Unity中,我们可以使用OpenCVforUnity插件(需导入)。-步骤:a.用户书写:使用LineRenderer或Texture2D记录用户输入,并保存为图片。b.预处理:将图片二值化,调整大小(如28x28像素),归一化等。c.训练模型:使用OpenCV的机器学习模块(如SVM、KNN)或深度学习模块(需提前训练好模型)。注意,训练过程可以在Unity外部进行,然后将训练好的模型导入Unity使用。d.识别:将预处理后的图片输入模型,得到预测结果。方法二:使用SentisUnity官方深度学习推理库,如引用[1])-步骤:a.同样需要用户书写并生成图片。b.预处理:调整图片大小,归一化等,使其符合模型输入要求。c.使用预训练的ONNX模型(例如一个训练好的字母识别模型)进行推理。d.在Unity中通过Sentis加载ONNX模型,输入预处理后的图片,得到输出。方法三:使用TensorFlow.NET或ML.NET(需要引入相应插件)-类似方法二,但使用不同的机器学习库。考虑到用户只需要识别字母'i'(实际上,单独识别一个字母没有意义,通常需要识别多个字母,但用户需求可能是识别字母'i',我们可以将其扩展为识别小写字母,或者仅针对'i'训练一个二分类模型?),但更常见的是识别多个字符。因此,我们假设用户需要识别多个字母,而'i'是其中之一。数据准备:-训练模型需要数据集,如EMNIST(ExtendedMNIST,包含字母和数字)。-如果仅识别一个字母,我们可以收集用户书写的'i'和其他字母(作为负样本)来训练一个二分类模型,但这样应用场景有限。建议步骤:1.在Unity中实现绘图板:-使用LineRenderer或直接在Texture2D上绘制(如引用[4]的方法获取绘制对象)。-将绘制的笔迹保存为Texture2D。2.预处理:-将Texture2D转换为灰度图,然后二值化(将背景设为黑色,笔迹设为白色)。-将图片大小调整为模型输入所需尺寸(如28x28)。-将像素值归一化到0-1或-1到1之间。3.模型训练(如果使用自定义模型):-使用公开数据集(如EMNISTLetters)训练一个分类模型(如CNN),并导出为ONNX格式(用于Sentis)或OpenCV可加载的格式(如.xml,用于SVM)。-如果使用OpenCV的KNN,可以直接使用EMNIST数据集训练并保存模型。4.在Unity中加载模型并进行推理。5.输出结果。由于用户需求是识别手写体'i',我们可以先实现一个简单的原型,使用OpenCV的KNN(因为KNN对于小规模数据简单有效)或一个小的神经网络(使用Sentis)。具体实现(以OpenCV为例):步骤1:创建绘图板-创建一个画布(Canvas),在画布上放置一个RawImage作为绘图区域。-使用脚本监听鼠标事件,在鼠标按下和拖动时,在RawImage上绘制线条(可以使用GL或Graphics.DrawLine,但更简单的是使用一个Texture2D,在鼠标移动的连续点之间画线)。步骤2:保存绘制的图像-将RawImage上的Texture2D保存为一个图片文件(或直接处理内存中的纹理)。步骤3:预处理-使用OpenCVforUnity插件对保存的图片进行以下处理:a.转为灰度图b.二值化(使用阈值,比如THRESH_BINARY_INV,使得笔迹为白色,背景为黑色)c.裁剪和调整大小:找到笔迹的边界框,裁剪出字母区域,然后缩放到28x28像素(与训练数据一致)d.将图像数据转换为特征向量(对于KNN,需要将28x28的图像展平为784维向量)步骤4:加载KNN模型(需提前训练好)-使用EMNIST数据集训练KNN模型(在外部训练),将模型保存为.xml文件,然后在Unity中加载。-或者,我们可以在Unity中训练模型(但训练过程通常较慢,不推荐在运行时训练),所以最好是提前训练好。步骤5:预测-将预处理后的特征向量输入KNN模型,找到最近的K个邻居,投票得出预测结果。步骤6:显示预测结果。如果用户希望使用深度学习方法,则可以使用一个简单的CNN模型(如LeNet-5)在EMNIST上训练,导出ONNX,然后用Sentis加载。考虑到用户可能希望快速实现,这里给出使用OpenCV的KNN方法(因为OpenCVforUnity插件使用相对简单,且训练好的KNN模型较小,推理速度快)。注意事项:-训练KNN模型时,使用EMNIST的字母数据集(包含26个字母的大小写,共52类),但用户可能只需要小写字母,我们可以只选择小写字母(EMNISTByClass包含所有类别,ByMerge则合并大小写,注意区分)。-如果用户只要求识别'i',我们可以训练一个二分类模型(区分'i'和非'i'),但这样需要负样本(其他字母),且应用场景有限,建议还是使用多分类模型。因此,我们推荐使用一个多分类模型(识别26个小写字母),这样用户不仅可以识别'i',还可以识别其他字母。实现步骤详细说明:1.训练模型(在Unity外部,使用Python和OpenCV):-下载EMNIST数据集(ByClass或ByMerge,这里建议使用EMNISTLetters,它包含26个字母(大小写合并为26类,即不区分大小写),但我们需要小写,所以可以选择EMNISTBalanced(包含数字、大写和小写)或EMNISTLetters(只包含字母,且大小写合并)。为了简单,我们使用EMNISTLetters(包含26类,每类大小写合并,所以实际是大小写混合,但标签不区分大小写)。但是,用户写的是小写'i',所以我们需要确保训练数据中有足够的小写'i'样本。-实际上,EMNISTLetters的标签是0-25分别对应A到Z(不区分大小写,但数据中同时包含大小写)。我们可以使用它,因为小写'i'和大写'I'在形状上相似(但实际不同,所以需要调整)。另一种选择是使用EMNISTByClass(包含62类:10数字+26大写+26小写),这样我们可以单独识别小写字母。-我们选择EMNISTByClass,并只取小写字母(类36-61,共26类,其中小写'i'是第44类(0-9数字,10-35大写字母,36-61小写字母,所以小写a是36,b是37,...,i是44,...,z是61)。-训练步骤:a.加载EMNISTByClass数据集(训练集)。b.提取小写字母部分(标签36-61),并重新映射标签为0-25(表示26个小写字母)。c.训练KNN分类器(k可以设为5)。d.保存模型为knn_emnist.xml。2.在Unity中:-导入OpenCVforUnity插件(从AssetStore)。-编写绘图板脚本(DrawingBoard.cs):-使用Texture2D作为画布,通过鼠标/触摸输入绘制线条。-提供清除按钮和识别按钮。-编写识别脚本(OCR.cs):-在点击识别按钮时,获取Texture2D,并预处理(灰度、二值化、调整大小等)。-加载knn_emnist.xml模型。-将预处理后的图像展平为特征向量,并输入KNN模型进行预测。-输出预测的字母。3.预处理细节:-由于用户绘制的字母可能是任意大小和位置,预处理需要:a.反转颜色:因为我们绘制时可能是黑色背景白色笔迹,但EMNIST是黑色背景白色笔迹(但训练时是白色背景黑色笔迹?注意EMNIST的原始图像是白底黑字,但通常我们在训练前会反转成黑底白字,和MNIST一致)。所以我们的预处理步骤需要保持一致:-将用户绘制的图片转为灰度。-二值化(THRESH_BINARY_INV):这样笔迹变成白色(255),背景黑色(0),与训练数据一致(训练数据是黑底白字?实际上,EMNIST原始数据是白底黑字,但通常预处理时会反转成黑底白字。所以我们的训练数据是黑底白字(即背景为0,笔迹为255))。b.裁剪:找到笔迹的边界框(非黑色区域),然后裁剪出这个区域,并保留一定边距。c.将裁剪后的字母放入一个28x28的图像中心(保持比例,避免变形)。具体做法:计算边界框的宽高比,然后按比例调整大小,放在一个28x28的黑色背景的中心。d.展平为784维向量,并归一化(将像素值从0-255归一化到0-1)。4.预测:-使用KNN的findNearest方法,输入特征向量,得到预测的类别标签(0-25),然后映射为字母(0->'a',1->'b',...,8->'i',...)。代码示例(Unity部分):由于代码较长,这里只给出关键步骤的伪代码。DrawingBoard.cs:voidStart(){texture=newTexture2D(width,height);//初始化纹理为黑色//设置给RawImage}voidUpdate(){if(鼠标按下){在texture上绘制白色线条(从上一个鼠标位置到当前位置)}}OCR.cs:publicvoidRecognize(){//1.获取DrawingBoard的Texture2D//2.预处理MatsourceMat=newMat(texture.height,texture.width,CvType.CV_8UC4);Utils.texture2DToMat(texture,sourceMat);Matgray=newMat();Imgproc.cvtColor(sourceMat,gray,Imgproc.COLOR_RGBA2GRAY);Matbinary=newMat();Imgproc.threshold(gray,binary,0,255,Imgproc.THRESH_BINARY_INV|Imgproc.THRESH_OTSU);//3.查找轮廓,获取边界框List<MatOfPoint>contours=newList<MatOfPoint>();Mathierarchy=newMat();Imgproc.findContours(binary,contours,hierarchy,Imgproc.RETR_EXTERNAL,Imgproc.CHAIN_APPROX_SIMPLE);//找到最大的轮廓(按面积)//获取边界框RectboundRect=Imgproc.boundingRect(contours[0]);//裁剪出字母区域Matcropped=newMat(binary,boundRect);//4.将裁剪后的字母放入28x28的中心intnewSize=20;//先缩放到20x20,然后放在28x28的中心,周围留4像素的边Matresized=newMat();Imgproc.resize(cropped,resized,newSize(newSize,newSize));Matcentered=newMat(28,28,CvType.CV_8UC1,newScalar(0));//全黑背景//计算放置的位置(偏移量)intoffsetX=(28-newSize)/2;intoffsetY=(28-newSize)/2;Matroi=newMat(centered,newRect(offsetX,offsetY,newSize,newSize));resized.copyTo(roi);//5.展平并归一化MatfloatMat=newMat();centered.convertTo(floatMat,CvType.CV_32F);//转为浮点MatfeatureVector=floatMat.reshape(1,1);//1行784列featureVector.convertTo(featureVector,CvType.CV_32F);//已经是浮点,确保归一化Core.normalize(featureVector,featureVector,0,1,Core.NORM_MINMAX);//6.加载KNN模型(提前放在Assets/StreamingAssets下)KNearestknn=KNearest.load(Application.streamingAssetsPath+"/knn_emnist.xml");//7.预测Matresults=newMat();floatresult=knn.findNearest(featureVector,5,results);intlabel=(int)result;charpredictedChar=(char)('a'+label);//因为我们的标签0对应a,1对应b,...Debug.Log("Predicted:"+predictedChar);}注意:以上代码仅为示例,实际使用需要调整。另外,如果用户希望使用深度学习模型(如CNN),我们可以用类似的方法训练一个CNN模型(使用TensorFlow或PyTorch),然后导出为ONNX,在Unity中使用Sentis加载和推理。步骤类似,只是预处理后输入到CNN模型中。总结:两种主要方法:1.传统机器学习:OpenCV的KNN(简单,速度快,但准确率可能不如深度学习)2.深度学习:使用Sentis加载预训练的ONNX模型(准确率高,但模型较大,推理稍慢)根据用户需求,如果只是识别一个字母,KNN可能足够。但为了更好的扩展性和准确率,推荐使用深度学习模型。相关问题:1.如何获取手写字母数据集?2.在Unity中如何实现绘图板?3.如何训练一个手写字母识别模型?4.如何将训练好的模型集成到Unity中?5.预处理步骤的具体实现细节有哪些?由于用户当前问题是实现方法,我们给出了两种方法的步骤,并详细描述了其中一种(OpenCVKNN)的实现过程。因此,回答如下:</think>在Unity中实现手写体字母"i"的识别,可参考以下分步实现方案。核心思路是:**采集用户笔迹→图像预处理→机器学习模型识别**。以下是具体实现方法: --- ### 一、实现步骤 #### 1. **笔迹采集(Unity绘图板)** - 使用`LineRenderer`或`Texture2D`实现绘图功能: ```csharp // 使用LineRenderer记录笔迹 LineRenderer lineRenderer; void Start() { lineRenderer = gameObject.AddComponent<LineRenderer>(); lineRenderer.startWidth = 0.1f; } void Update() { if (Input.GetMouseButton(0)) { Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition); mousePos.z = 0; lineRenderer.positionCount++; lineRenderer.SetPosition(lineRenderer.positionCount - 1, mousePos); } } ``` - 或通过`Texture2D`绘制到画布(更高精度): ```csharp public RawImage drawingCanvas; Texture2D drawTexture; void Start() { drawTexture = new Texture2D(256, 256); drawingCanvas.texture = drawTexture; } public void DrawAtPosition(Vector2 pos) { int x = (int)(pos.x * drawTexture.width); int y = (int)(pos.y * drawTexture.height); drawTexture.SetPixel(x, y, Color.black); drawTexture.Apply(); } ``` #### 2. **笔迹转图像** - 截图保存笔迹(关键步骤): ```csharp IEnumerator CaptureDrawing() { yield return new WaitForEndOfFrame(); Texture2D tex = new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false); tex.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0); tex.Apply(); // 保存tex用于识别 } ``` #### 3. **图像预处理** - 使用OpenCV for Unity插件(需导入)进行: - **二值化**:转黑白图像 - **尺寸归一化**:缩放至标准大小(如28x28像素) - **降噪**:去除孤立像素点 ```csharp // OpenCV预处理示例 Mat sourceMat = new Mat(tex.height, tex.width, CvType.CV_8UC4); Utils.texture2DToMat(tex, sourceMat); Mat grayMat = new Mat(); Imgproc.cvtColor(sourceMat, grayMat, Imgproc.COLOR_RGBA2GRAY); Mat binaryMat = new Mat(); Imgproc.threshold(grayMat, binaryMat, 127, 255, Imgproc.THRESH_BINARY); ``` #### 4. **识别模型集成** - **方案1:OpenCV机器学习(轻量级)** - 训练KNN/SVM分类器(需EMNIST字母数据集) -Unity中加载预训练模型: ```csharp KNearest knn = KNearest.load(Application.dataPath + "/knn_model.xml"); Mat input = preprocessedMat.reshape(1, 1); float result = knn.predict(input); ``` - **方案2:深度学习(高精度)** - 使用Unity Sentis引擎(推荐): 1. 导出PyTorch/TensorFlow训练的ONNX模型(需包含"i"识别) 2. 在Unity中加载模型: ```csharp var model = ModelLoader.Load(Application.streamingAssetsPath + "/letter_recognition.onnx"); var engine = WorkerFactory.CreateWorker(BackendType.GPUCompute, model); engine.SetInput("input", preprocessedTensor); engine.Execute(); Tensor output = engine.PeekOutput("output"); ``` 3. 输出层解析:取概率最高的字母类别 --- ### 二、关键技术选型 | 方法 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | **OpenCV+KNN** | 部署简单,CPU即可运行 | 精度较低(约85%~90%) | 简单字母识别 | | **Sentis+ONNX** | 精度高(>95%),支持复杂模型 | 需GPU支持,模型较大 | 高精度需求 | | **TensorFlow Lite** | 移动端优化 | Android/iOS需额外配置 | 移动应用 | --- ### 三、优化建议 1. **数据增强**:在Unity中生成旋转/缩放的手写"i"样本[^2] 2. **双阶段识别**: - 先检测笔画数量("i"需两笔:竖+点) - 再通过模型验证 3. **实时反馈**:在绘图时显示识别结果(参考引用[1]的交互设计) --- ### 四、示例流程 ```mermaid graph TD A[用户手写输入] --> B(笔迹转图像) B --> C{预处理} C --> D[二值化] C --> E[尺寸归一化] C --> F[中心化] D --> G[模型推理] E --> G F --> G G --> H[OpenCV/Sentis] H --> I(输出识别结果) ``` --- ### 常见问题解决 - **误识别问题**:增加负样本(如将"l"、"j"标记为非"i") - **笔画断裂**:在`LineRenderer`中启用`cornerVertices`平滑 - **性能瓶颈**:移动端建议使用二值化+小尺寸模型 > 提示:训练模型时需包含不同书写风格的"i"(如带钩/不带钩),数据集推荐使用[EMNIST Letters](https://www.nist.gov/itl/products-and-services/emnist-dataset)。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

安东尼王

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值