一、整体思路
首先单片机获取摄像头的图片并存储,对图片中的像素做二值化处理,然后和数字的点阵数组作比较,找到偏差最小的一个数字作为识别结果。这个想法是我自然而然想出来的,并没有深刻思考效率和正确率,所以它的误差比较大,主要和图片中数字的位置和字形相关。
二、识别过程
1.获取图片并存储
STM32F103C8T6单片机运行时的数据内存是20KB,而一帧分辨率为128*160的图片大小为40KB,所以不能直接完整地存储下来,我的解决办法是只存储32*64大小的部分图片,也可以认为这一小部分是识别区域。准备白纸写下待识别的数字
差不多这样就可以,不过那个数字1还是写小了,就是需要描粗一点,不然二值化处理的时候就成全白了,然后拍摄图片,将纸上的数字放到识别区域内(红线框起来的,也就是数字7)
关于单片机连接摄像头和显示屏的方法我已经发过了,这个识别区域也只是在正常显示的基础上加了几条线,所以下面写一下图片显示函数的代码
void ov7670() { while(OV7670_VS()==0){}; OV7670_W_WRST(0); OV7670_W_WRST(1); OV7670_W_WEN(1); Delay_us(40000); while(OV7670_VS()==0); OV7670_W_WRST(0); OV7670_W_WRST(1); OV7670_W_WEN(0); Delay_us(50000); OV7670_W_OE(0); OV7670_W_RRST(0); OV7670_W_RCLK(0); Delay_us(20000); OV7670_W_RCLK(1); Delay_us(20000); OV7670_W_RCLK(0); Delay_us(20000); OV7670_W_RRST(1); Delay_us(20000); OV7670_W_RCLK(1); set_windows(0,0,lcddev.width-1,lcddev.height-1); for(h=0;h<160;h++)//128 { for(w=0;w<128;w++) { OV7670_W_RCLK(0); value1 = (uint8_t)GPIOA->IDR & 0xff; OV7670_W_RCLK(1); OV7670_W_RCLK(0); value2 = (uint8_t)GPIOA->IDR & 0xff; OV7670_W_RCLK(1); color = (value1 << 8) | value2; TFT_WRITE_u16_DATA(color); if((w<weight) && (h<hight))//设置weight为32,hight为64 { dat[h*4+w/8][w%8] = color;//把左上角识别区域内的像素存储下来 } } } OV7670_W_OE(1); } void boundary(uint8_t a,uint8_t b)//划红线的函数,a的值是weight,b的值是hight { uint8_t i; set_windows(0,0,a,0); for(i=0; i<=(a+1); i++) { TFT_WRITE_u16_DATA(0xf800); } set_windows(0,0,0,b); for(i=0; i<=(b+1); i++) { TFT_WRITE_u16_DATA(0xf800); } set_windows(0,b,a,b); for(i=0; i<=(a+1); i++) { TFT_WRITE_u16_DATA(0xf800); } set_windows(a,0,a,b); for(i=0; i<=(b+1); i++) { TFT_WRITE_u16_DATA(0xf800); } }
2.处理图片
对存储的部分图片进行二值化处理,让图片变成黑白两色,便于接下来的识别。二值化的依据是像素点的灰度值,所以先计算出每个像素点的灰度值。对于RGB565格式的像素,采用式子((R>>8)*77+(G>>3)*150+(B<<3)*29+128)/256可以大概算出灰度值,代码如下
uint8_t togrey(uint16_t color)//适用于RGB565格式的像素值 { uint8_t grey; grey = (uint8_t)((((color&0xf800)>>8)*77+((color&0x07e0)>>3)*150+((color&0x001f)<<3)*29+128)/256); return grey; }
之后根据灰度值进行二值化处理,将灰度值大于100的像素点改成白色,其余改成黑色。在数字的点阵数组中,一个像素点用一个二进制位表示,0代表白色1代表黑色,相邻八个像素点组成一个字节,所以也把存储图片的像素点用这种方式表示出来,即白色的像素点用0表示,黑色的像素点用1表示,并把相邻八个像素点组合,代码如下
void dis()//二值化处理 { uint8_t w,h,n=0; set_windows(0,0,weight-1,hight-1); for(h=0; h<hight; h++) { for(w=0; w<weight; w++) { n <<= 1; if(togrey(dat[h*4+w/8][w%8])>100) { TFT_WRITE_u16_DATA(0xffff); } else { TFT_WRITE_u16_DATA(0x0000); n |= 0x01; } if(w%8==7) { number[h*4+w/8] = n;//运行这一句之后得到可以和点阵数组作比较的数组 n=0; } } } }
如果把二值化处理之后的数组再次显示出来,就是下图红线框里的样子,中间是识别结果。
3.识别数字
我认为,识别是显示的逆过程,一个数字显示在屏幕上,是将一些固定的点显示成黑色或白色,反过来,只要找到这些位置相似的点,就可以认为它是这个数字。在TFT屏幕上一个分辨率为32*64大小的数字0,其点阵数组如下
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0xE0,0x00, 0x00,0x1F,0xF8,0x00,0x00,0x3C,0x1E,0x00,0x00,0x70,0x0F,0x00,0x00,0xE0,0x07,0x00, 0x01,0xE0,0x03,0x80,0x03,0xC0,0x03,0xC0,0x03,0xC0,0x01,0xC0,0x07,0x80,0x01,0xE0, 0x07,0x80,0x00,0xE0,0x07,0x00,0x00,0xE0,0x0F,0x00,0x00,0xF0,0x0F,0x00,0x00,0xF0, 0x0F,0x00,0x00,0xF0,0x0F,0x00,0x00,0x70,0x1E,0x00,0x00,0x78,0x1E,0x00,0x00,0x78, 0x1E,0x00,0x00,0x78,0x1E,0x00,0x00,0x78,0x1E,0x00,0x00,0x78,0x1E,0x00,0x00,0x78, 0x1E,0x00,0x00,0x78,0x1E,0x00,0x00,0x78,0x1E,0x00,0x00,0x78,0x1E,0x00,0x00,0x78, 0x1E,0x00,0x00,0x78,0x1E,0x00,0x00,0x78,0x1E,0x00,0x00,0x78,0x1E,0x00,0x00,0x78, 0x0F,0x00,0x00,0x70,0x0F,0x00,0x00,0xF0,0x0F,0x00,0x00,0xF0,0x0F,0x00,0x00,0xF0, 0x07,0x00,0x00,0xE0,0x07,0x80,0x01,0xE0,0x07,0x80,0x01,0xE0,0x03,0xC0,0x01,0xC0, 0x03,0xC0,0x03,0xC0,0x01,0xE0,0x03,0x80,0x00,0xE0,0x07,0x00,0x00,0x70,0x0F,0x00, 0x00,0x3C,0x1E,0x00,0x00,0x1F,0xF8,0x00,0x00,0x07,0xE0,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}//数字0
这个点阵数组和上面二值化处理后得到的数组大小一样,所以可以使其两两比较,选出其中差别最小的,认为它们是同一个数字。我的比较方法是,将同位置像素点的差值的绝对值加在一起,然后选出十个值当中最小的,代码如下
uint8_t compare() { uint8_t RGB=0; int rgb[10]={0}; uint16_t i=0,j=0; for(j=0; j<10; j++) { for(i=0; i<256; i++) { if(number[i] >= num[j][i])//取绝对值 { rgb[j] += number[i] - num[j][i]; } else rgb[j] += num[j][i] - number[i] ; } } for(j=1; j<10; j++)//选出值最小的 { if(rgb[RGB] > rgb[j]) RGB = j; } return RGB; }
4.结果
图片的显示和处理应该明确地区分开,我选用了一个按键,每按一次就在显示和处理当中切换一次,按键在下图红色椭圆圈起来的位置。
在识别区域内的数字清晰之后,按下按键开始识别,识别结果就会显示在屏幕中间的位置,如下
可见,识别的结果对于数字的字形和摆放位置关联较大。这个数字识别是关于摄像头和单片机的简单应用,没有用到更深刻更高效的算法,所以它的误差是显而易见的,写这三篇文章是想记录一下学习过程,现在F103C8T6接OV7670的资料在网上是比较少的,我在调试摄像头时花费了很长的时间,经历最多的无非就是失败了,我已经习惯了它的花屏、没反应等情况,虽然它桀骜不驯,但是好在它没有坏,从头至尾调试几百次,只为一句莫问收获,但问耕耘,以此和看到这篇文章的同学们共勉。