用Java写一个KNN手写识别程序


此程序要实现的功能有:在0-8的可选数字中,可选择指定数字,在书写面板上用鼠标书写该数字
并录入素材库。

在书写面板上书写任意数字,点击识别按钮后程序能识别出对应数字。

 

要实现这样一个程序,我们需要:一个UI类用于展示UI面板、一个算法类用于实现KNN算法识别、一个基础数据接口用于保存程序的基本数据。


1.数据接口类BasicData 

//基本数据
public interface BasicData {
	//窗体
    int framewidth = 450;
    int frameheight = 550;
    //控制面板
    int panelwidth = 450;
    int panelheight = 50;
    //行距
    int linesize = 20;
    //行数
    int line = 20;
    //起始坐标
    int x0=25,y0=25;
}

2.UI类

要展示的UI分为三个部分:

(1)窗体

(2)控制面板:含可选择数字序列、开始按钮、录入按钮、识别按钮

(3)书写面板:由一个19x19的网格构成,书写面板的网格是每次启动程序都要画好的,因此我们用一个子类继承JPanel,重写paint()方法写入网格的绘制。

书写面板类

//书写面板类
public class DrawPanel extends JPanel implements BasicData{
	//重写paint方法
	public void paint(Graphics g) {
		super.paint(g);//执行父类方法
		g.setColor(Color.BLACK);//设置画笔颜色
		//绘制网格
		for(int i=0;i<line;i++) {
			g.drawLine(x0, y0 + i * linesize, x0 + (line - 1) * linesize, y0 + i * linesize);
            g.drawLine(x0 + i * linesize, y0, x0 + i * linesize, y0 + (line - 1) * linesize);
		}
	}
}

UI类

//UI面板
public class UI implements BasicData{
	public void showUI() {
		//窗体
		JFrame jf = new JFrame("knn手写识别");
		jf.setSize(framewidth,frameheight);//设置窗体大小
		jf.setDefaultCloseOperation(3);//后台关闭
		jf.setLocationRelativeTo(null);//居中显示
		jf.setLayout(new BorderLayout());//边框式布局
		//控制面板
		JPanel cpanel = new JPanel();
		cpanel.setPreferredSize(new Dimension(panelwidth,panelheight));//设置面板大小
		jf.add(cpanel,BorderLayout.NORTH);//将面板添加到窗体上方位置
		//书写面板
		DrawPanel dpanel = new DrawPanel();
		jf.add(dpanel,BorderLayout.CENTER);//将面板添加到窗体中间位置
		//控制面板中的按钮组件
		//下拉选择组件
		Integer num[] = {0,1,2,3,4,5,6,7,8};
		JComboBox<Integer> jcb = new JComboBox<>(num);
		cpanel.add(jcb);
		//按钮
		JButton jbu1 = new JButton("开始");
		JButton jbu2 = new JButton("录入");
		JButton jbu3 = new JButton("识别");
		cpanel.add(jbu1);
		cpanel.add(jbu2);
		cpanel.add(jbu3);
		//监听器
		Listener ls = new Listener();
		jbu1.addActionListener(ls);
		jbu2.addActionListener(ls);
		jbu3.addActionListener(ls);
		dpanel.addMouseListener(ls);
		dpanel.addMouseMotionListener(ls);
		
		jf.setVisible(true);//设置窗体可见
		
		/*要在监听器类中完成书写、识别和录入功能,
		 * 因此要把面板的画笔对象传入监听器类以获取书写素材
		 * 也要把选择数字序列组件也传入监听器类以获取选择录入的数字,
		 * 还要把书写面板对象传入监听器类以实现面板的刷新
		 */
		Graphics g = dpanel.getGraphics();//获取书写面板的画笔对象
		ls.setGraphics(g);
		ls.setJComboBox(jcb);
		ls.setDrawPanel(dpanel);
	}
	
	public static void main(String[] args) {
		new UI().showUI();
	}
}

       

                                                      

监听器类

监听器类要实现的功能:

(1)图像的绘制:通过鼠标拖动方法实现

(2)素材录入:用二维数组实现

(3)图像识别 :用KNN算法实现

(1)图像绘制

先定义变量

	private int x1,y1,x2,y2;//画笔的起点坐标和终点坐标
	private int[][] site = new int[line-1][line-1];//标记数组,每一个网格为一个标记点
    private String info;//所选择按钮上的文字
	private boolean start = false;//是否开始程序

选择不同按钮执行不同方法,此处先写点击“开始”按钮

    public void actionPerformed(ActionEvent e) {
		info = e.getActionCommand();//获取所点击按钮的文字
		switch(info) {
			case "开始":
				start = true;//更改开始标记
				//把site数组所有值重置为0
				for(int i=0;i<line-1;i++) {
					Arrays.fill(site[i],0);
				}
				dpanel.repaint();//刷新面板
				break;
			case "录入":
				break;
			case "识别":
				break;
		}
		
	}

 图像的绘制

    //鼠标按下时获取起始坐标
	public void mousePressed(MouseEvent e) {
		if(start == true) {
			//获取起点坐标
			x1 = e.getX();
			y1 = e.getY();
		}
	}
	//鼠标拖动画线可以看成一段段直线的微元,不断更新起点坐标为终点坐标实现画线
	public void mouseDragged(MouseEvent e) {
		if(start = true) {
			//获取终点坐标
			x2 = e.getX();
			y2 = e.getY();
			g.drawLine(x1, y1, x2, y2);//画线
			//19x19的网格对应19x19的二维数组site,线条经过的网格位置对应的数组位置标记为1
			if(x1>=x0&&x1<=x0+(line-1)*linesize && y1>=y0&&y1<=y0+(line-1)*linesize) {//在网格线内书写
				if((x1-x0)/linesize<line-1 && (y1-y0)/linesize<line-1){//数组越界判断,线是(line)20,格是(line-1)19,则数组最大下标是(line-1-1)18
                    site[(y1-y0)/linesize][(x1-x0)/linesize] = 1;//标记访问
                }
			}
            //将起点坐标更新为终点坐标,实现微元画线
			x1 = x2;
			y1 = y2;
		}
	}

(2)素材录入

素材录入的思路:每一次画线会获得一个标记完的二维数组,将该二维数组以字节流的方式存入素材文件,通过将素材所要标记的数字加入到对素材的命名中,以此来判断此段素材用于识别哪个数字,对用于标记相同数字的素材,用UI类的hashcode()方法来作为素材名的后缀,可以避免素材名重复。如对于数字2的两段素材名可以为:2-178887853,2-178887854。

录入

    //录入
    public void Record(int[][] site,String selectedNum) throws IOException {
        //文件路径,每一个素材对应一个txt文件存在素材文件夹中,文件名是素材对应数字 + 调用UID类获取哈希码(可避免文件名重复)
        String filename = "D:\\java\\KNN手写识别\\素材\\"+selectedNum+"-"+new UID().hashCode()+".txt";
        File file = new File(filename);
        //fos要写入的文件file,若不存在则先新建再写入,若存在则直接写入覆盖之前的内容
        FileOutputStream fos = new FileOutputStream(file);
        //用字节流将二维数组写入文件
        for(int i=0;i<line-1;i++){
            for(int j=0;j<line-1;j++){
                fos.write(site[i][j]);
            }
        }
        fos.flush();
        fos.close();
        System.out.println("素材"+selectedNum+"录入成功");
    }

更新actionPerformed()方法

               case "录入":
                start = false;//停止书写
                String selectedNum = jcb.getSelectedItem().toString();//获取选择录入的数字
                try {
                    Record(site,selectedNum);//调用录入方法
                } catch (IOException ex) {
                    throw new RuntimeException(ex);
                }
                break;

(3)识别

识别方法要用到KNN算法,思路会相对复杂一点:

a.当获取到要识别图像的二维数组target后,对于素材库中每一个素材文件,用字节流读取获取其二维数组site,将target数组与site数组进行矩阵相减 ,再对相减后的矩阵的每一个值,取其绝对值并加和,得到的最终值result即为两个数组的契合度,result越小,target与site的契合度越高。

b.如果只是得到result是不够的,我们还需知道target是与哪个数字的素材进行计算得到的result,否则即使得到的result再小,由于不知道是与哪个数字的契合度高,仍无法识别目标。因此我们用一个二元结构体ResultData来保存 每一个site的标记数字sampleNum 和 target与该site计算得到后的result值。

把目标文件与所有素材文件计算后得到的ResultData都存入一个动态数组中。

c.接下来部分是KNN算法的体现:对该动态数组进行排序,取result最小的前5个ResultData,如果这5个的sampleNum都不重复,则取result最小的对应的sampleNum为结果数字。如果5个sampleNum中出现重复数字,则取重复数最多的sampleNum作为结果数字。这个对重复数字的判断可以用一个哈希表来实现。

d.最后可以写一个展示窗体,用于显示最终结果。

接下来是算法实现,首先我们需要一个结构体ResultData

class ResultData{
    public int sampleNum;//素材标记数字
    public int distance;//素材数组与目标数组计算出的契合度
    public ResultData(int sampleNum,int distance){
        this.sampleNum = sampleNum;
        this.distance = distance;
    }
}

其次如a中所说我们需要一个计算素材数组与目标数组契合度(在KNN中叫距离)的方法。

    public int getDistance(int[][] site,int[][] sampleSite){
        int result = 0;//契合度
        for(int i=0;i<line-1;i++){
            for(int j=0;j<line-1;j++){
                int a = site[i][j] - sampleSite[i][j];
                //取绝对值加和
                if(a>=0){
                    result += a;
                }else{
                    result -= a;
                }
            }
        }
        return result;
    }

接下来是一个对结构体数组进行排序的快排算法

    //快速排序-从小到大
    public void fastSort(List<ResultData> nums, int low, int high) {
        if (low >= high) return;
        int i = low, j = high;
        ResultData t;
        ResultData tmp = nums.get(low); // 使用 get 方法获取列表中的元素
        while (i < j) {
            while (i < j && nums.get(j).distance >= tmp.distance) j--;
            while (i < j && nums.get(i).distance <= tmp.distance) i++;
            if (i < j) {
                t = nums.get(i);
                nums.set(i, nums.get(j)); // 使用 set 方法设置列表中的元素
                nums.set(j, t);
            }
        }
        nums.set(low, nums.get(i));
        nums.set(i, tmp);
        fastSort(nums, low, i - 1);
        fastSort(nums, i + 1, high);
    }

然后是展示结果窗体的方法

    //显示结果窗体
    public void showResult(int data){
        JFrame jf = new JFrame("识别结果");
        jf.setSize(300,250);
        jf.setDefaultCloseOperation(3);
        jf.setLocationRelativeTo(null);
        JLabel jl = new JLabel("识别成功!结果为"+data);//标签
        jl.setFont(new Font("宋体", Font.BOLD, 30));//设置字体样式
        jl.setHorizontalAlignment(JLabel.CENTER);//设置居中对齐
        jf.add(jl);
        jf.setVisible(true); 
    }

最后是识别的代码实现

    //识别
    public void Recognize(int[][] site) throws IOException {
        //存放所有素材的文件
        File file = new File("D:\\java\\KNN手写识别\\素材\\");
        //list()方法返回一个字符串数组,为该文件目录下所有子文件的名字
        String[] sampleName = file.list();
        //用一个结构体动态数组存储结果值
        resultList = new ArrayList<ResultData>();
        //逐个取出每个素材中存储的二维数组,与要识别的数组进行契合度计算,将结果记录到动态数组中
        for(int i=0;i<sampleName.length;i++){
            //素材文件名首字符为素材标记数字,以此用substring切分出字符串首字符获得当前素材对应的数字,parseInt方法把数字名由字符串转成整型
            int sampleNum = Integer.parseInt(sampleName[i].substring(0,1));
            //得到当前素材的文件路径
            File sample = new File("D:\\java\\KNN手写识别\\素材\\"+sampleName[i]);
            FileInputStream fis = new FileInputStream(sample);
            //用字节流读取出当前素材的二维数组
            int[][] sampleSite = new int[line-1][line-1];//素材二维数组
            int m=0,n=0;
            int b = fis.read();
            while(b!=-1){//读取为空时返回-1
                sampleSite[m][n] = b;
                n++;
                if(n==line-1){//换行
                    m++;
                    n=0;
                }
                b = fis.read();
            }
            fis.close();
            //进行契合度计算
            int distance = getDistance(site,sampleSite);
            //将结果记入动态数组
            resultList.add(new ResultData(sampleNum,distance));
        }
        //对动态数组进行快排
        fastSort(resultList,0,resultList.size()-1);
        //对排序后的结果数组,取前5个值,用一张哈希表记录5个值中不重复数字的出现个数,取出现个数最多的那一个。
        HashMap<Integer,Integer> hm = new HashMap<>();//<素材标记数字,出现次数>
        for(int i=0;i<5;i++){
            int key = resultList.get(i).sampleNum;
            if(!hm.containsKey(key)){
                hm.put(key,1);
            }else{
                hm.put(key,hm.get(key)+1);
            }
        }
        //遍历哈希表找出现次数最多的,finNum默认值为resultList.get(0).sampleNum是因为若最小5个值都不相同,则取最小的
        int finNum = resultList.get(0).sampleNum,finCount=hm.get(finNum);
        for(int key:hm.keySet()){//遍历哈希表key值
            if(hm.get(key)>finCount){
                finCount = hm.get(key);
                finNum = key;
            }
        }
        System.out.println(finNum);
        showResult(finNum);//展示结果窗体
    }

别忘了更新actionPerformed()方法

               case "识别":
                start = false;
                dpanel.repaint();
                try {
                    Recognize(site);
                } catch (IOException ex) {
                    throw new RuntimeException(ex);
                }
                break;

效果展示:

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值