前言
今天要讲的呢,是自己制作手写图片,并处理成MNIST的标准格式,输入到我们训练好的模型来做识别,看看效果怎样,一直用别人提供的东西,调调api什么的总感觉参与感少了点哈哈。
本来这一部分我是打算给加到MNIST数据集介绍和朴素贝叶斯那两期后面作为扩展阅读的,因为觉得应该是蛮简单的一件事,没想到实际做下来又花了我2天的时间。遇到了不少难点和细节处理,所以最后决定独立一篇文章算了。
今天这篇呢,会涉及到图像处理的一些相关概念,比如RGB转灰度,还有图像在计算机里面的存储方式;也会涉及到一些计算机二进制的处理,蛮多知识盲点的,网上资料也不齐全,所以接下来会整理一下,结合自己的理解把这个学习过程记录下来。特别注意的是,网上相关代码基本都是python的,都是调用封装好的工具类,我这会用scala来全部手动实现一遍,希望下次跟我一样的小菜鸟遇到这些问题的时候不会像我那样束手无策了哈哈哈哈哈哈哈哈
制作手写图片
这里为了省事,我就直接用ps新建一张28 x 28像素的图片来写数字了,不过一开始做出来的图片我拿去识别效果并不好,所以拿了训练数据输出灰度矩阵,再对比自己手写的数字输出的灰度矩阵,发现识别不准的原因主要出现在以下两个地方:
- 手写数字要尽可能居中,且不要占满整个图片,边缘要留出部分空白
- 画笔像素过大,边缘像素有所偏差。
对于上述问题,经过我多次尝试后,画笔参数调成柔边
,2像素
效果较好,创建过程如图:
画笔的选择:
图片的灰度处理
手写出来的图片看着是黑白的,其实是RGB真彩图片,要变成我们MNIST的标准格式,还要先把24位深的图片处理成8位深的灰度图。
几种灰度化方法
- 分量法:使用RGB三个分量中的一个作为灰度图的灰度值。
- 最值法:使用RGB三个分量中最大值或最小值作为灰度图的灰度值。
- 均值法:使用RGB三个分量的平均值作为灰度图的灰度值。
- 加权法:由于人眼颜色敏感度不同,按下一定的权值对RGB三分量进行加权平均能得到较合理的灰度图像。一般情况按照:
Y = 0.30R + 0.59G + 0.11B
。
从网上看到的资料来说哈,据说是加权法出来的图片效果最好。
实际上java有自己的灰度化办法:
1 | public void grayImage() throws IOException{ |
看代码就是创建一个灰度图对象,然后把原图的RGB像素点强塞进去,输出成灰度图。出来的效果相当简陋,对于人来说看起来颜色比加权法出来的要糟糕不少,下面给出对比图片:
原图:
Java自带的处理出来的灰度图:
自己实现的加权法出来的灰度图:
很明显的,加权法出来的图片肉眼看起来更清晰,因此最后我选择用scala来实现图片的灰度化
scala中实现图片灰度化
说是scala实现吧,其实还是调了java的包哈哈哈,完整代码我放到了github上,顺手做了个批量读取文件夹里图片拼装成MNIST数据格式,所以分了3个class,于下列代码有点不一样,所以想要看如何实现还是去看完整代码吧:完整代码
这里一步步来,首先是读取图片文件:
1 | val file = new File(path) |
把图片灰度化:
1 | /* 把图片转成灰度值图片 |
到这一步就已经得到了一个灰度化图片了,接下来我们只需要按照MNIST格式
把灰度图拼装成MNIST格式
按照MNIST官网的格式,把一些数据信息转成byte数组拼接起来,再把灰度图的像素值以8位每个点(1byte)拼接起来就可以了,这里我直接给出代码,为了方便我还写了个批量读取文件夹内的图片,把这些图片全部转成byte数组再一一进行拼接。
1 | package SparkMLlib.Base |
生成的图片和文件如下:
标签起名直接做成图片文件名,如果有多个“1”的图片数据呢,把文件名弄成1-1.jpg
,1-2.jpg
,······这样就好了
关于图像处理的一些知识扩展
图像位深
一个像素用多少位表示,这里的位
,只是针对每一个像素点而言的。考虑到位深度平均分给R, G, B和Alpha,而只有RGB可以相互组合成颜色。所以4位颜色的图,它的位深度是4,只有2的4次幂种颜色,即16种颜色或16种灰度等级 ) 。8位颜色的图,位深度就是8,用2的8次幂表示,它含有256种颜色 ( 或256种灰度等级 )。24位颜色可称之为真彩色,位深度是24,它能组合成2的24次幂种颜色,即:16777216种颜色 ( 或称千万种颜色 ),超过了人眼能够分辨的颜色数量。当我们用24位来记录颜色时,实际上是以2^(8×3),即红、绿、蓝 ( RGB ) 三基色各以2的8次幂,256种颜色而存在的,三色组合就形成一千六百万种颜色。
图像在计算机的存储格式
一般来说,我们自己做的图片都是24位的RGB真彩图,在上述代码中,img.getRGB(i,j)
这里get出来的只有一个int值,其实他已经把RGB三原色的值全放在里面了,由高位到低位排列下来,分别是R
8位 + G
8位 + B
8位,所以我们通过右移操作符 >>
每次移动8位,在& 0xFF
一下,就能取出RGB分别的Int
颜色值来了。而最高的32位深的图片,只是在RGB的基础三多加了一个alpha透明度通道。对于32位的像素值,里面的排列顺序由高位到低位排列下来,分别是alpha
8位 + R
8位 + G
8位 + B
8位,同样可以按位把真实值取出来。
关于 & 0xFF 操作
回想一下大学学的计算机导论,有一条很重要但是不被当时我所重视的知识:计算机中整数以补码的形式存储。正数的补码与原码一致,这没什么问题,当时负数的补码却是原码的反码+1,这也导致了为什么我们没做& 0xFF
操作的时候直接byte转int会由出现负数的原因之一(另一个原因是java里面int都是带了符号位,表示的数字范围在 -128 ~ 127,之间,而实际像素值是 0 ~ 255的正整数)。
为什么byte类型的数字要 &0xff
再赋值给int类型呢,其本质原因就是想保持二进制补码的一致性。例如二进制的的129为10000001
,8位byte就能存下这个数字,当时若是直接转int,出来的结果却是-127,其原因就在于,因为当系统检测到byte可能会转化成int或者说byte与int类型进行运算的时候,就会将byte的内存空间高位补1(也就是按符号位补位)扩充到32位,变成1111111111111111111111111 10000001
,再参与运算,这样其二进制补码其实已经不一致了。而进行与操作 &0xff
则是这样的:
1111111111111111111111111 10000001 & 11111111 = 000000000000000000000000 10000001
显然,这就是为什么我们在byte转成int的时候,要做一下 &0xff
操作了