unity 手写板 截取游戏画面 识别手写文字 全家桶
Posted 吴梓穆
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了unity 手写板 截取游戏画面 识别手写文字 全家桶相关的知识,希望对你有一定的参考价值。
要求离线识别,不能接外网
识别手写文字分为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 手写板 截取游戏画面 识别手写文字 全家桶的主要内容,如果未能解决你的问题,请参考以下文章