C# PaddleInference OCR 验证码识别

这篇具有很好参考价值的文章主要介绍了C# PaddleInference OCR 验证码识别。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

说明

效果

项目

测试图片

代码

下载 


说明

C# PaddleInference OCR 验证码识别

自己训练的模型,只针对测试图片类型,准确率99%

效果

C# PaddleInference OCR 验证码识别,OpenCV,AI,C#,ocr,C# 验证码识别

C# PaddleInference OCR 验证码识别,OpenCV,AI,C#,ocr,C# 验证码识别  

项目

VS2022+.net4.8+OpenCvSharp4+Sdcb.PaddleInference

C# PaddleInference OCR 验证码识别,OpenCV,AI,C#,ocr,C# 验证码识别

测试图片

C# PaddleInference OCR 验证码识别,OpenCV,AI,C#,ocr,C# 验证码识别

代码

using OpenCvSharp;
using Sdcb.PaddleInference.Native;
using Sdcb.PaddleInference;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;

namespace PaddleInference_OCR_验证码识别
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        Bitmap bmp;
        string fileFilter = "*.*|*.bmp;*.jpg;*.jpeg;*.tiff;*.tiff;*.png";
        string img = "";
        string startupPath = "";

        OcrShape recShape = new OcrShape(3, 320, 48);
        PaddlePredictor rec_predictor;

        public IReadOnlyList<string> Labels;

        DateTime dt1 = DateTime.Now;
        DateTime dt2 = DateTime.Now;


        private void button1_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.Filter = fileFilter;
            if (ofd.ShowDialog() != DialogResult.OK) return;

            pictureBox1.Image = null;

            img = ofd.FileName;
            bmp = new Bitmap(img);
            pictureBox1.Image = new Bitmap(img);
            textBox1.Text = "";
        }

        private unsafe void Form1_Load(object sender, EventArgs e)
        {

            string startupPath = Application.StartupPath;
            IntPtr det_ptr = PaddleNative.PD_ConfigCreate();
            Encoding PaddleEncoding = Environment.OSVersion.Platform == PlatformID.Win32NT ? Encoding.GetEncoding(CultureInfo.CurrentCulture.TextInfo.ANSICodePage) : Encoding.UTF8;

            //识别模型
            IntPtr rec_ptr = PaddleNative.PD_ConfigCreate();
            String rec_programPath = startupPath + "\\model\\inference.pdmodel";
            String rec_paramsPath = startupPath + "\\model\\inference.pdiparams";
            byte[] rec_programBytes = PaddleEncoding.GetBytes(rec_programPath);
            byte[] rec_paramsBytes = PaddleEncoding.GetBytes(rec_paramsPath);
            fixed (byte* rec_programPtr = rec_programBytes)
            fixed (byte* rec_paramsPtr = rec_paramsBytes)
            {
                PaddleNative.PD_ConfigSetModel(rec_ptr, (IntPtr)rec_programPtr, (IntPtr)rec_paramsPtr);
            }

            rec_predictor = new PaddlePredictor(PaddleNative.PD_PredictorCreate(rec_ptr));

            //Labels
            String labelsPath = startupPath + "\\ppocr_keys.txt";
            Stream Steam = new FileStream(labelsPath, FileMode.Open, FileAccess.Read, FileShare.Read);
            StreamReader reader = new StreamReader(Steam);
            List<string> tempList = new List<string>();
            while (!reader.EndOfStream)
            {
                tempList.Add(reader.ReadLine());
            }
            reader.Dispose();
            Steam.Dispose();
            Labels = tempList;
        }

        private void button3_Click(object sender, EventArgs e)
        {
            if (pictureBox1.Image == null)
            {
                return;
            }
            dt1 = DateTime.Now;
            var src = OpenCvSharp.Extensions.BitmapConverter.ToMat(bmp);

            int modelHeight = recShape.Height;
            int maxWidth = (int)Math.Ceiling(1.0 * src.Width / src.Height * modelHeight);

            Mat channel3 = new Mat();
            if (src.Channels() == 4)
            {
                channel3 = src.CvtColor(ColorConversionCodes.RGBA2BGR);
            }
            else if (src.Channels() == 3)
            {
                channel3 = src.Clone();
            }
            else if (src.Channels() == 1)
            {
                channel3 = src.CvtColor(ColorConversionCodes.GRAY2RGB);
            }
            else
            {
                throw new Exception("Unexpect src channel: {" + src.Channels() + "}, allow: (1/3/4)");
            }

            Mat resized = ResizePadding(channel3, modelHeight, maxWidth);
            Mat normalize = Normalize(resized);

            using (PaddleTensor input = rec_predictor.GetInputTensor(rec_predictor.InputNames[0]))
            {
                int channel = normalize.Channels();
                input.Shape = new[] { 1, channel, modelHeight, maxWidth };
                float[] data = ExtractMat(normalize, channel, modelHeight, maxWidth);
                input.SetData(data);
            }

            normalize.Dispose();
            resized.Dispose();

            if (!rec_predictor.Run())
            {
                throw new Exception($"PaddlePredictor(Recognizer) run failed.");
            }

            using (PaddleTensor output = rec_predictor.GetOutputTensor(rec_predictor.OutputNames[0]))
            {
                float[] data = output.GetData<float>();
                int[] shape = output.Shape;
                GCHandle dataHandle = default;
                try
                {
                    dataHandle = GCHandle.Alloc(data, GCHandleType.Pinned);
                    IntPtr dataPtr = dataHandle.AddrOfPinnedObject();

                    int labelCount = shape[2];
                    int charCount = shape[1];

                    StringBuilder sbInfo = new StringBuilder();

                    for (int i = 0; i < shape[0]; i++)
                    {
                        StringBuilder sb = new StringBuilder();
                        int lastIndex = 0;
                        float score = 0;
                        for (int n = 0; n < charCount; ++n)
                        {
                            Mat mat = new Mat(1, labelCount, MatType.CV_32FC1, dataPtr + (n + i * charCount) * labelCount * sizeof(float));
                            int[] maxIdx = new int[2];
                            mat.MinMaxIdx(out double _, out double maxVal, new int[0], maxIdx);
                            if (maxIdx[1] > 0 && (!(n > 0 && maxIdx[1] == lastIndex)))
                            {
                                score += (float)maxVal;
                                sb.Append(GetLabelByIndex(maxIdx[1]));
                            }
                            lastIndex = maxIdx[1];
                            mat.Dispose();
                        }
                        sbInfo.AppendLine("Text:" + sb.ToString());
                        sbInfo.AppendLine("Score:" + score / sb.Length);
                    }

                    dt2 = DateTime.Now;
                    sbInfo.AppendLine("-----------------------------------\n");
                    sbInfo.AppendLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"));
                    sbInfo.AppendLine("耗时:" + (dt2 - dt1).TotalMilliseconds + "ms\n");
                    textBox1.Text = sbInfo.ToString();
                }
                finally
                {
                    dataHandle.Free();
                }
            }

        }

        string GetLabelByIndex(int x)
        {
            if (x > 0 && x <= Labels.Count)
            {
                return Labels[x - 1];
            }
            else if (x == Labels.Count + 1)
            {
                return "";
            }
            else
            {
                throw new Exception("Unable to GetLabelByIndex: index {" + x + "} out of range {" + Labels.Count + "}, OCR model or labels not matched?");
            }
        }

        private Mat ResizePadding(Mat src, int height, int targetWidth)
        {
            OpenCvSharp.Size size = src.Size();
            float whRatio = 1.0f * size.Width / size.Height;
            int width = (int)Math.Ceiling(height * whRatio);

            if (width == targetWidth)
            {
                return src.Resize(new OpenCvSharp.Size(width, height));
            }
            else
            {
                Mat resized = src.Resize(new OpenCvSharp.Size(width, height));
                return resized.CopyMakeBorder(0, 0, 0, targetWidth - width, BorderTypes.Constant, Scalar.Gray);
            }
        }

        private Mat Normalize(Mat src)
        {
            Mat normalized = new Mat();
            src.ConvertTo(normalized, MatType.CV_32FC3, 1.0 / 255);
            Mat[] bgr = normalized.Split();
            float[] scales = new[] { 1 / 0.229f, 1 / 0.224f, 1 / 0.225f };
            float[] means = new[] { 0.485f, 0.456f, 0.406f };
            for (int i = 0; i < bgr.Length; ++i)
            {
                bgr[i].ConvertTo(bgr[i], MatType.CV_32FC1, 1.0 * scales[i], (0.0 - means[i]) * scales[i]);
            }
            normalized.Dispose();
            Mat dest = new Mat();
            Cv2.Merge(bgr, dest);
            foreach (Mat channel in bgr)
            {
                channel.Dispose();
            }
            return dest;
        }

        private float[] ExtractMat(Mat mat, int channel, int height, int width)
        {
            float[] result = new float[1 * channel * width * height];
            GCHandle resultHandle = GCHandle.Alloc(result, GCHandleType.Pinned);
            IntPtr resultPtr = resultHandle.AddrOfPinnedObject();
            try
            {

                Mat src = mat.Clone();
                if (src.Channels() != channel)
                {
                    throw new Exception($"src channel={src.Channels()}, expected {channel}");
                }
                for (int c = 0; c < channel; ++c)
                {
                    Mat dest = new Mat(height, width, MatType.CV_32FC1, resultPtr + c * height * width * sizeof(float));
                    Cv2.ExtractChannel(src, dest, c);
                    dest.Dispose();
                }
                return result;
            }
            finally
            {
                resultHandle.Free();
            }
        }

        private float[] ExtractMat(Mat[] srcs, int channel, int height, int width)
        {
            float[] result = new float[srcs.Length * channel * width * height];
            GCHandle resultHandle = GCHandle.Alloc(result, GCHandleType.Pinned);
            IntPtr resultPtr = resultHandle.AddrOfPinnedObject();
            try
            {
                for (int i = 0; i < srcs.Length; ++i)
                {
                    Mat src = srcs[i];
                    if (src.Channels() != channel)
                    {
                        throw new Exception($"src[{i}] channel={src.Channels()}, expected {channel}");
                    }
                    for (int c = 0; c < channel; ++c)
                    {
                        Mat dest = new Mat(height, width, MatType.CV_32FC1, resultPtr + (c + i * channel) * height * width * sizeof(float));
                        Cv2.ExtractChannel(src, dest, c);
                        dest.Dispose();
                    }
                }
                return result;
            }
            finally
            {
                resultHandle.Free();
            }
        }

      
    }
}
 

using OpenCvSharp;
using Sdcb.PaddleInference.Native;
using Sdcb.PaddleInference;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;

namespace PaddleInference_OCR_验证码识别
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        Bitmap bmp;
        string fileFilter = "*.*|*.bmp;*.jpg;*.jpeg;*.tiff;*.tiff;*.png";
        string img = "";
        string startupPath = "";

        OcrShape recShape = new OcrShape(3, 320, 48);
        PaddlePredictor rec_predictor;

        public IReadOnlyList<string> Labels;

        DateTime dt1 = DateTime.Now;
        DateTime dt2 = DateTime.Now;


        private void button1_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.Filter = fileFilter;
            if (ofd.ShowDialog() != DialogResult.OK) return;

            pictureBox1.Image = null;

            img = ofd.FileName;
            bmp = new Bitmap(img);
            pictureBox1.Image = new Bitmap(img);
            textBox1.Text = "";
        }



        private unsafe void Form1_Load(object sender, EventArgs e)
        {

            string startupPath = Application.StartupPath;
            IntPtr det_ptr = PaddleNative.PD_ConfigCreate();
            Encoding PaddleEncoding = Environment.OSVersion.Platform == PlatformID.Win32NT ? Encoding.GetEncoding(CultureInfo.CurrentCulture.TextInfo.ANSICodePage) : Encoding.UTF8;

            //识别模型
            IntPtr rec_ptr = PaddleNative.PD_ConfigCreate();
            String rec_programPath = startupPath + "\\model\\inference.pdmodel";
            String rec_paramsPath = startupPath + "\\model\\inference.pdiparams";
            byte[] rec_programBytes = PaddleEncoding.GetBytes(rec_programPath);
            byte[] rec_paramsBytes = PaddleEncoding.GetBytes(rec_paramsPath);
            fixed (byte* rec_programPtr = rec_programBytes)
            fixed (byte* rec_paramsPtr = rec_paramsBytes)
            {
                PaddleNative.PD_ConfigSetModel(rec_ptr, (IntPtr)rec_programPtr, (IntPtr)rec_paramsPtr);
            }

            rec_predictor = new PaddlePredictor(PaddleNative.PD_PredictorCreate(rec_ptr));

            //Labels
            String labelsPath = startupPath + "\\ppocr_keys.txt";
            Stream Steam = new FileStream(labelsPath, FileMode.Open, FileAccess.Read, FileShare.Read);
            StreamReader reader = new StreamReader(Steam);
            List<string> tempList = new List<string>();
            while (!reader.EndOfStream)
            {
                tempList.Add(reader.ReadLine());
            }
            reader.Dispose();
            Steam.Dispose();
            Labels = tempList;
        }

        private void button3_Click(object sender, EventArgs e)
        {
            if (pictureBox1.Image == null)
            {
                return;
            }
            dt1 = DateTime.Now;
            var src = OpenCvSharp.Extensions.BitmapConverter.ToMat(bmp);

            int modelHeight = recShape.Height;
            int maxWidth = (int)Math.Ceiling(1.0 * src.Width / src.Height * modelHeight);

            Mat channel3 = new Mat();
            if (src.Channels() == 4)
            {
                channel3 = src.CvtColor(ColorConversionCodes.RGBA2BGR);
            }
            else if (src.Channels() == 3)
            {
                channel3 = src.Clone();
            }
            else if (src.Channels() == 1)
            {
                channel3 = src.CvtColor(ColorConversionCodes.GRAY2RGB);
            }
            else
            {
                throw new Exception("Unexpect src channel: {" + src.Channels() + "}, allow: (1/3/4)");
            }

            Mat resized = ResizePadding(channel3, modelHeight, maxWidth);
            Mat normalize = Normalize(resized);

            using (PaddleTensor input = rec_predictor.GetInputTensor(rec_predictor.InputNames[0]))
            {
                int channel = normalize.Channels();
                input.Shape = new[] { 1, channel, modelHeight, maxWidth };
                float[] data = ExtractMat(normalize, channel, modelHeight, maxWidth);
                input.SetData(data);
            }

            normalize.Dispose();
            resized.Dispose();

            if (!rec_predictor.Run())
            {
                throw new Exception($"PaddlePredictor(Recognizer) run failed.");
            }

            using (PaddleTensor output = rec_predictor.GetOutputTensor(rec_predictor.OutputNames[0]))
            {
                float[] data = output.GetData<float>();
                int[] shape = output.Shape;
                GCHandle dataHandle = default;
                try
                {
                    dataHandle = GCHandle.Alloc(data, GCHandleType.Pinned);
                    IntPtr dataPtr = dataHandle.AddrOfPinnedObject();

                    int labelCount = shape[2];
                    int charCount = shape[1];

                    StringBuilder sbInfo = new StringBuilder();

                    for (int i = 0; i < shape[0]; i++)
                    {
                        StringBuilder sb = new StringBuilder();
                        int lastIndex = 0;
                        float score = 0;
                        for (int n = 0; n < charCount; ++n)
                        {
                            Mat mat = new Mat(1, labelCount, MatType.CV_32FC1, dataPtr + (n + i * charCount) * labelCount * sizeof(float));
                            int[] maxIdx = new int[2];
                            mat.MinMaxIdx(out double _, out double maxVal, new int[0], maxIdx);
                            if (maxIdx[1] > 0 && (!(n > 0 && maxIdx[1] == lastIndex)))
                            {
                                score += (float)maxVal;
                                sb.Append(GetLabelByIndex(maxIdx[1]));
                            }
                            lastIndex = maxIdx[1];
                            mat.Dispose();
                        }
                        sbInfo.AppendLine("Text:" + sb.ToString());
                        sbInfo.AppendLine("Score:" + score / sb.Length);
                    }

                    dt2 = DateTime.Now;
                    sbInfo.AppendLine("-----------------------------------\n");
                    sbInfo.AppendLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"));
                    sbInfo.AppendLine("耗时:" + (dt2 - dt1).TotalMilliseconds + "ms\n");
                    textBox1.Text = sbInfo.ToString();
                }
                finally
                {
                    dataHandle.Free();
                }
            }

        }

        string GetLabelByIndex(int x)
        {
            if (x > 0 && x <= Labels.Count)
            {
                return Labels[x - 1];
            }
            else if (x == Labels.Count + 1)
            {
                return "";
            }
            else
            {
                throw new Exception("Unable to GetLabelByIndex: index {" + x + "} out of range {" + Labels.Count + "}, OCR model or labels not matched?");
            }
        }

        private Mat ResizePadding(Mat src, int height, int targetWidth)
        {
            OpenCvSharp.Size size = src.Size();
            float whRatio = 1.0f * size.Width / size.Height;
            int width = (int)Math.Ceiling(height * whRatio);

            if (width == targetWidth)
            {
                return src.Resize(new OpenCvSharp.Size(width, height));
            }
            else
            {
                Mat resized = src.Resize(new OpenCvSharp.Size(width, height));
                return resized.CopyMakeBorder(0, 0, 0, targetWidth - width, BorderTypes.Constant, Scalar.Gray);
            }
        }

        private Mat Normalize(Mat src)
        {
            Mat normalized = new Mat();
            src.ConvertTo(normalized, MatType.CV_32FC3, 1.0 / 255);
            Mat[] bgr = normalized.Split();
            float[] scales = new[] { 1 / 0.229f, 1 / 0.224f, 1 / 0.225f };
            float[] means = new[] { 0.485f, 0.456f, 0.406f };
            for (int i = 0; i < bgr.Length; ++i)
            {
                bgr[i].ConvertTo(bgr[i], MatType.CV_32FC1, 1.0 * scales[i], (0.0 - means[i]) * scales[i]);
            }
            normalized.Dispose();
            Mat dest = new Mat();
            Cv2.Merge(bgr, dest);
            foreach (Mat channel in bgr)
            {
                channel.Dispose();
            }
            return dest;
        }



        private float[] ExtractMat(Mat mat, int channel, int height, int width)
        {
            float[] result = new float[1 * channel * width * height];
            GCHandle resultHandle = GCHandle.Alloc(result, GCHandleType.Pinned);
            IntPtr resultPtr = resultHandle.AddrOfPinnedObject();
            try
            {

                Mat src = mat.Clone();
                if (src.Channels() != channel)
                {
                    throw new Exception($"src channel={src.Channels()}, expected {channel}");
                }
                for (int c = 0; c < channel; ++c)
                {
                    Mat dest = new Mat(height, width, MatType.CV_32FC1, resultPtr + c * height * width * sizeof(float));
                    Cv2.ExtractChannel(src, dest, c);
                    dest.Dispose();
                }
                return result;
            }
            finally
            {
                resultHandle.Free();
            }
        }



        private float[] ExtractMat(Mat[] srcs, int channel, int height, int width)
        {
            float[] result = new float[srcs.Length * channel * width * height];
            GCHandle resultHandle = GCHandle.Alloc(result, GCHandleType.Pinned);
            IntPtr resultPtr = resultHandle.AddrOfPinnedObject();
            try
            {
                for (int i = 0; i < srcs.Length; ++i)
                {
                    Mat src = srcs[i];
                    if (src.Channels() != channel)
                    {
                        throw new Exception($"src[{i}] channel={src.Channels()}, expected {channel}");
                    }
                    for (int c = 0; c < channel; ++c)
                    {
                        Mat dest = new Mat(height, width, MatType.CV_32FC1, resultPtr + (c + i * channel) * height * width * sizeof(float));
                        Cv2.ExtractChannel(src, dest, c);
                        dest.Dispose();
                    }
                }
                return result;
            }
            finally
            {
                resultHandle.Free();
            }
        }

      
    }
}

下载 

Demo下载文章来源地址https://www.toymoban.com/news/detail-562520.html

到了这里,关于C# PaddleInference OCR 验证码识别的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • OpenCV项目1-OCR识别

    文章处理的图片: 思路:我们拿到一张图片不是直接就识别的,而是做一些处理,去除我们不需要的信息,保留我们所需要的信息,再做识别。 通常要做的是对图片进行灰度化处理,再进行滤波,形态学等等去除掉不要的信息。 因为在处理的过程中,我们要对每一步处理后

    2024年02月07日
    浏览(39)
  • 【Python】【OpenCV】OCR识别(三)——字符识别

    通过上一篇博客,我们成功将有角度的图片进行“摆正”,接下来我们来提取图片中的文字。 我们使用Tesseract来处理图片并提取文字,相关下载安装请参考:Python下Tesseract Ocr引擎及安装介绍 - 黯然销魂掌2015 - 博客园 (cnblogs.com) 同时我们需要下载第三方Lib——pytesseract,使用

    2024年02月02日
    浏览(45)
  • OpenCV(九)--文字扫描OCR识别

    步骤:边缘检测+计算轮廓+变换+OCR OCR识别 识别结果: 完整代码

    2024年01月18日
    浏览(39)
  • 【Python】【OpenCV】OCR识别(一)

    接着练手图像处理例子   抛开网上截图进行OCR识别,更多的图源来自于我们的手机,相机等等设备,而得到的图片都并非是板正的,大多随手一拍的图源都是带有角度的,所以我们需要先将图像进行摆正。 首先先对图像进行预处理,上代码:         1、使用Canny来进行边缘

    2024年02月03日
    浏览(43)
  • Python通用验证码识别OCR库ddddocr的安装使用

    之前写了一篇关于java使用tess4j进行图片文字识别.md的,对于应付简单的数字识别还是能应付,但总体效果、识别率很一般,后来同事找到了这一篇写的,(亲测好用便捷)Python通用验证码识别OCR库ddddocr的安装使用教程,试用了下确实效果要好很多,因此也记录一下,算是白嫖了

    2024年02月05日
    浏览(49)
  • 【Python】【OpenCV】OCR识别(二)——透视变换

    对于OCR技术在处理有角度有偏差的图像时是比较困难的,而水平的图像使用OCR识别准确度会高很多,因为文本通常是水平排列的,而OCR算法一般会假设文本是水平的。 针对上述情况,所以我们在处理有角度的图象时,需要将图像“摆正”,将使用到getPerspectiveTransform方法和

    2024年02月03日
    浏览(41)
  • 验证码识别DLL ,滑块识别SDK,OCR图片转文字,机器视觉找物品

    验证码识别DLL ,滑块识别SDK 你们用过哪些OCR提取文字,识图DLL,比如Opencv,Labview机器视觉找物品之类?   

    2024年02月11日
    浏览(37)
  • 【爬虫】8.1. 深度使用tesseract-OCR技术识别图形验证码

    前言:本片文章是基于我之前发的一篇文章《【爬虫】8.1. 使用OCR技术识别图形验证码》而写的,链接为: 【爬虫】8.1. 使用OCR技术识别图形验证码,前面这篇文章比较基础。入门了tesseract-OCR技术之后对它比较感兴趣,故继续深度学习以下。为了衔接比较好,故本篇文章前面

    2024年02月09日
    浏览(45)
  • opencv-信用卡数字识别-模板匹配ocr

    2024年02月11日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包