unity 手写板 截取游戏画面 识别手写文字 全家桶

Posted 吴梓穆

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了unity 手写板 截取游戏画面 识别手写文字 全家桶相关的知识,希望对你有一定的参考价值。

要求离线识别,不能接外网
识别手写文字分为3步

  1. 第一步,做手写板
  2. 第二步,截取游戏画面
  3. 第三步,识别

第一步,做个手写板
我们可以使用LineRender实现手写板功能
具体思想为,读取鼠标位置,当鼠标的位移超过阈值时,为LineRender添加一个新点,听起来不靠谱,但实际上效果还不错

public class DrawLine : MonoBehaviour

    /// <summary>
    /// 线条的父物体
    /// </summary>
    private Transform lineParent;
    /// <summary>
    /// 线的材质 修改这个材质的颜色可以更改线的颜色
    /// </summary>
    private Material lineMaterial;
    /// <summary>
    /// 画笔的宽度
    /// </summary>
    public float paintSize = 3;
    /// <summary>
    /// 如果鼠标按下
    /// </summary>
    public bool isMouseDown = false;
    /// <summary>
    /// 当前的线条绘制器
    /// </summary>
    private LineRenderer currentLine;
    /// <summary>
    /// 当前的鼠标位置
    /// </summary>
    private Vector3 currentMousePostion;
    /// <summary>
    /// 上一个鼠标的位置
    /// </summary>
    private Vector3 lastMosuePostion;
    /// <summary>
    /// 当前笔记的所有点
    /// </summary>
    private List<Vector3> points = new List<Vector3>();

    private new void Awake()
    
        base.Awake();
        lineParent = GameObject.Find("LineParent").transform;
        lineMaterial = Resources.Load<Material>("Material/LineMaterial");
    
    private void Update()
    
        if (Input.GetMouseButtonDown(0))
        
            GameObject newLine = new GameObject();
            newLine.transform.parent = lineParent;
            currentLine = newLine.AddComponent<LineRenderer>();
            currentLine.materials[0] = lineMaterial;
            currentLine.startWidth = paintSize;
            currentLine.endWidth = paintSize;
            isMouseDown = true;            
        

        if (isMouseDown)
        
            //当前的鼠标的位置
            currentMousePostion = Input.mousePosition;
            currentMousePostion = new Vector3(currentMousePostion.x, currentMousePostion.y, 10);
            //超出阈值,添加一个新的点,阈值自己定
            if (Vector3.Distance(currentMousePostion, lastMosuePostion) >= 1f)
            
                AddLinePoint(currentMousePostion);
                lastMosuePostion = currentMousePostion;
            
        

        if (Input.GetMouseButtonUp(0))
        
            isMouseDown = false;
            currentLine = null;
            lastMosuePostion = Vector3.zero;
            points.Clear();
        
    

    /// <summary>
    /// 添加线的点
    /// </summary>
    private void AddLinePoint(Vector3 pos)
    
        points.Add(pos);
        currentLine.positionCount = points.Count;
        currentLine.SetPositions(points.ToArray());
    
    /// <summary>
    /// 清理已经画好的点
    /// </summary>
    public void ClearLine()
    
        for (int i = 0; i < lineParent.childCount; i++)
        
            Destroy(lineParent.GetChild(i).gameObject);
        
    


Unity中的相关物体:所有的线条都是这个物体的子物体

注意,我是用的分辨率是1920*1080,使用屏幕坐标画出的线非常巨大,每个像素都是1米,所以我将主相机放在x960y540的的位置,如果你觉得太大,可以使用射线检测一个背景Plane,用射线击中点,代替屏幕坐标,可以有效降低线的尺寸
效果:


第二步 截取游戏画面
有三种不同的截图方式可供我们选择
1 使用ScreenCapture

Texture2D texture2D = ScreenCapture.CaptureScreenshotAsTexture();

但只能全屏截图,会截到不需要的UI

2使用Texture2D读取屏幕像素

        Rect rect = new Rect(0, 0, Screen.width, Screen.height );        
        Texture2D resultTexture = new Texture2D(Screen.width, Screen.height);
        resultTexture.ReadPixels(rect, 0, 0);//将自动读取屏幕上的像素
        resultTexture.Apply();

可以自定义读取的矩形区域,Rect的四个参数分别为 起始坐标X,起始坐标Y,从起始坐标向右读多少像素,从起始坐标向上读多少相许 ,例子中的起始坐标(0,0),是屏幕的左下角

但是这么做的问题在于,依然会截到UI,而且如果是多屏幕,他截取的那个屏幕不一定,根据实验应该是最后激活的屏幕

3使用相机的RenderTexture
本方法基于方法2,只不过Textrue2D不再读取屏幕像素,而是读取RenderTexture

        //声明一个临时的渲染纹理,如果不用临时纹理,也可以在Assets里新建一个RenderTexure,拖给相机的targetTexure属性,但是注意有RenderTexture的相机将不会向game窗口输出画面
        RenderTexture renderTexture = new RenderTexture(Screen.width,Screen.height,0);
        uiCamera_2.targetTexture = renderTexture;
        uiCamera_2.Render();//让相机渲染一帧
        //激活渲染纹理,Textrue2D将读取这个纹理的像素,而不读取屏幕,如果不激活,将读取屏幕
        RenderTexture.active = renderTexture; 

        Rect rect = new Rect(0, 0, Screen.width, Screen.height );        
        Texture2D resultTexture = new Texture2D(Screen.width, Screen.height);
        resultTexture.ReadPixels(rect, 0, 0);
        resultTexture.Apply();

        uiCamera_2.targetTexture = null;//将相机画面重新输出到game窗口

这么做的好处是,截到的画面只和相机拍到的画面有关,因此可以屏蔽UI,并且不受Display窗口限制


第三步,文字识别
研究了一些方法,发现只用unity是不行的(主要是因为识别文字的程序包和Untiy冲突),需要外接c#程序,我使用的是winform程序,通过Unity调用winform程序,通过udp通信完成文字识别

首先新建一个winform程序
右键解决方案,打开Nuget包管理

切换到浏览,搜索PaddleOCRSharp,并安装

引入命名空间

using PaddleOCRSharp;

编写代码

 private void GetOCR()
        
            Bitmap bitmap = new Bitmap("d://ocr.png");
            //启动引擎  引擎启动比较耗时,可以提前启动,待程序完成再释放
            OCRModelConfig config = null;
            OCRParameter oCRParameter = new OCRParameter();
            OCRResult oCRResult = new OCRResult();
            PaddleOCREngine engine = new PaddleOCREngine(config,oCRParameter);
            
            oCRResult = engine.DetectText(bitmap);

            Console.WriteLine(oCRResult.Text);
            //释放引擎
            engine.Dispose();
			//释放图片
            bitmap.Dispose();
        

此处只是识别演示代码,可以实现最基本的识别

注意:字不可以太大,也不可以太小,会影响结果,不能识别连笔

工程完整代码
winform已经放入untiy,Untiy工程可以直接单独运行,无需启动winform
winform程序用于展示代码

免积分下载

OpenCV——识别手写体数字

这个是树莓派上运行的, opencv3

opencv提供了一张手写数字图片给我们,如下图所示,可以作为识别手写数字的样本库。

0到9共十个数字,每个数字有五行,一行100个数字。首先要把这5000个数字截取出来。

图片大小为1000*2000,则每个数字块大小为20*20。

1.截取样本并存储

以下代码为截取以上数字并将其存储在矩阵中的过程

训练的数据,一般都会是两个矩阵,一个矩阵存放着数据图像,另一个矩阵存放数据图像对应的数字

 Mat src = imread("sample.png");
    Mat grayImage;
    cvtColor(src, grayImage, CV_BGR2GRAY);
    threshold(grayImage, grayImage, 48, 255, CV_THRESH_BINARY);
    int p = 20;                                         //一个数字大小为20*20
    int m = grayImage.rows / p;          //横行的数字个数m
    int n  = grayImage.cols / p;            //纵列的数字个数n 
    Mat data, labels;                              //data存放样本数据,label为data样本所对应的数字

    for( int i = 0; i < n; i++){
        int y = i * p;                                   //纵列第i个数字开始的位置
        for(int j = 0; j < m; j++){
            int x = j * p;                               //横行第i个数字开始的位置
            Mat dst;
            grayImage(Range(x,x + p), Range(y, y + p)).copyTo(dst);
            
            data.push_back(dst.reshape(0,1));         //将20*20大小矩阵变为1*400 向量
            labels.push_back( j / 5);                           //对应数据向量存储的数字
        }
    }

    data.convertTo(data, CV_32F);                       //改变像素的数据类型为浮点型
    Mat trainData, trainLabels; 
    trainData = data(Range(0, 5000), Range::all()); 
    trainLabels = labels(Range(0, 5000), Range::all());


2.处理待识别数字的图像

//处理代检测图像
    Mat Image, dst;
    Image = imread("6.png");
    cvtColor(Image, Image, COLOR_BGR2GRAY);
    threshold(Image, Image, 48, 255, CV_THRESH_BINARY_INV);
    imshow("Image", Image);
    Image.copyTo(dst);
    vector< vector<Point> > contours;
    vector<Vec4i> hierarchy; 
    findContours(Image,contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
    vector<Point> point = contours[0];
    Rect rect = boundingRect(point);
    int x = rect.x, y = rect.y;
    int h=rect.height, w = rect.width;
   Mat now = dst(Range(x, x+h-1), Range(y, y+w-1));
    //dst(rect).copyTo(now);
    resize(now,now,Size(20,20));


3.使用knn算法进行识别,要将识别的图像也进行像训练样本一样的处理

我在运行程序时,一直有如下的错误,换了好几种处理图片的方式,仍然没有用 

Mat_<float>  nums;
    nums  = now.reshape(0,1);
    nums.convertTo(nums, CV_32F);
    imshow("待测图像", now);
   /* Mat mm;
     mm.push_back(now.reshape(0,1));
     mm.convertTo(mm,CV_32F);
     Mat nums = mm(Range(0,1),Range::all());
     /*float imagedata[20*20];
     for(int i =0; i < 20; i++){
       for(int j=0;j<20;j++){
                      imagedata[ i *20 +j] = now.data[i *20+j];
         }
   }  
   Mat nums(1,20*20, CV_32F, imagedata);*/

最后查看源代码才发现不是其他参数的问题:

///  错误 knn->findNearest(nums, 1, Mat());

Mat temp;
knn->findNearest(nums, 1, temp);
//要传入一个具体的Mat类型

最后的识别代码为

//创建knn分类器
    Ptr<ml::KNearest>  knn = (ml::KNearest::create()); 
    knn->setIsClassifier(true);
    Ptr<ml::TrainData> tData = ml::TrainData::create(trainData,ml::ROW_SAMPLE, trainLabels);
    knn->train(tData);
    Mat temp;
    float result = knn->findNearest(nums, 1, temp);
    
    cout << result<<endl;

检查了好多遍,也只是不能识别出所有

程序缺陷:待检测的图像处理问题。不能截取出合适的roi区域

 

再改进吧。

 

以上是关于unity 手写板 截取游戏画面 识别手写文字 全家桶的主要内容,如果未能解决你的问题,请参考以下文章

Keras深度学习实战(37)——手写文字识别

OpenCV——识别手写体数字

图像识别基于模板匹配之手写数字识别系统GUI界面

图像识别基于模板匹配之手写数字识别系统GUI界面

潦草手写体也能轻松识别,快速提取文字不用愁

潦草手写体也能轻松识别,快速提取文字不用愁