系列文章目录
文章目录
前言
在接下来的一系列博客中,我将带领大家一起探索一个全新的手写识别项目。这个项目的核心是KNN算法,而在最初的阶段,我们暂时不涉及复杂的机器学习和神经网络,这使得它成为一个入门门槛较低、适合更多初学者的学习项目。
在第一篇博客中,我将从搭建GUI界面开始,为大家展示这个项目的基本框架。我会简要介绍KNN算法,让大家对这个算法有一个初步的了解。这篇博客旨在为后续的学习奠定基础,为大家展示整个项目的轮廓。最终的项目成果在github上也会同步。每一篇文章都是项目完成的一部分,更新过程中可能会对之前的文章进行修改
一、初识KNN算法
当涉及到KNN的实现时,Java提供了灵活的数据结构和算法库,可以使用这些库来实现KNN算法。以下是一个简单的Java代码示例,演示了如何使用KNN算法对数据集进行分类。
在示例中,假设我们有一个包含样本数据的CSV文件,每行包含特征值和标签。这里使用Weka库中的IBk
类(Weka是一个用于机器学习和数据挖掘的Java库)来实现KNN。
首先,需要添加Weka库的依赖。这里假设已经添加了Weka的JAR文件到项目中。
1、KNN算法的Java实现示例
import weka.core.*;
import weka.core.converters.ConverterUtils.DataSource;
import weka.classifiers.lazy.IBk;
import java.io.File;
public class KNNExample {
public static void main(String[] args) {
try {
// 加载数据集
DataSource source = new DataSource("path/to/your/data.arff"); // 替换为你的数据集文件路径
Instances data = source.getDataSet();
// 设置类别属性(假设标签在最后一列)
data.setClassIndex(data.numAttributes() - 1);
// 构建KNN分类器
IBk knn = new IBk();
knn.buildClassifier(data);
// 创建一个测试样本(这里假设有一组特征值)
Instance testInstance = new DenseInstance(data.numAttributes());
testInstance.setDataset(data);
testInstance.setValue(0, 1.5); // 设置第一个特征值
// 设置其他特征值...
// 进行分类预测
double predictedClass = knn.classifyInstance(testInstance);
String predictedClassName = data.classAttribute().value((int) predictedClass);
System.out.println("预测类别: " + predictedClassName);
} catch (Exception e) {
e.printStackTrace();
}
}
}
这个例子中,需要替换path/to/your/data.csv
为你的数据集文件路径,并且设置测试样本的特征值。该代码加载CSV数据集,构建KNN分类器,然后使用测试样本进行分类预测,并输出预测结果。
二、项目整体结构
根据目前对这个项目的整体认知和把握,这个项目可以分成4个java文件分别是
- Main.java (用于构建GUI)
- Listener.java (用于实现鼠标监听器和图形的绘制和调用FileOp中的方法)
- KNN.java (用于实现KNN的核心算法)
- FileOp.java (用于实现与文件操作相关的识别和训练的方法)
当深入了解一个项目时,将重点放在GUI构建和事件处理上是很合理的。Main.java和Listener.java确实承担了构建GUI和用户交互的重要角色。在这篇文章中,GUI的构建和事件处理是核心所在。在后续的文章中,剩下几个文件的内容和功能进行详细的解释。
三、GUI的搭建
1、源代码:
import javax.swing.*;
import java.awt.*;
public class Main extends JPanel {
/**
* Generates a function comment for the given function body in a Markdown code block
* with the correct language syntax.
*
* @param args the command line arguments
*/
public static void main(String[] args) {
Main main = new Main();
main.initUI();
}
/**
* Initializes the user interface for the application.
*/
public void initUI() {
// Create a new JFrame
JFrame frame = new JFrame();
frame.setTitle("手写识别");
frame.setSize(410, 460);
frame.setResizable(false);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
// Use a BorderLayout for the main panel
this.setLayout(new BorderLayout());
// Create a panel for buttons using GridLayout
JPanel componentsPanel = new JPanel(new GridLayout(1, 4));
JButton recognizeButton = new JButton("识别");
JButton trainButton = new JButton("训练");
JButton clearButton = new JButton("重置");
recognizeButton.setPreferredSize(new Dimension(100, 30));
trainButton.setPreferredSize(new Dimension(100, 30));
clearButton.setPreferredSize(new Dimension(100, 30));
componentsPanel.add(recognizeButton);
componentsPanel.add(trainButton);
componentsPanel.add(clearButton);
JComboBox<String> comboBox = new JComboBox<>(new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"});
componentsPanel.add(comboBox);
// Create a panel for both buttonPanel in the same row using FlowLayout
JPanel rowPanel = new JPanel(new FlowLayout());
rowPanel.add(componentsPanel);
// Set the preferred size and background color of the main panel
this.setPreferredSize(new Dimension(400, 400));
this.setBackground(Color.WHITE);
// Add components to the main panel using BorderLayout regions
this.add(rowPanel, BorderLayout.NORTH);
// Add the main panel to the frame
frame.add(this);
// Make the frame visible
frame.setVisible(true);
// Create a new listener and register it with the buttons and main panel
Listener listener = new Listener(this);
recognizeButton.addActionListener(listener);
trainButton.addActionListener(listener);
clearButton.addActionListener(listener);
comboBox.addActionListener(listener);
this.addMouseListener(listener);
this.addMouseMotionListener(listener);
}
}
基于Swing库构建了一个简单的GUI界面,提供了识别和训练两个按钮,一个数字选择的下拉列表,并预留了一个黑色背景的绘图区域。通过事件监听器,可以响应用户的操作并触发相应的事件处理。
2、需要注意
在编写过程中,我也遇到了一些问题,下面分享给大家,需要注意的一些十分细节的地方。下面我们从头开始梳理
(1)关于为什么要继承自JPanel
在以前的诸多与GUI有关的项目中,我们的UI通常都是继承自JFrame,这就要说明一下JFrame与JPanel的区别。首先,我先演示一下继承自JFrame和继承自JPanel的区别是什么:
JFrame与JPanel的区别,这里简单的说一下,下面是GPT3.5的回答:
- 当涉及到图形用户界面(GUI)编程时:
JFrame
是用于创建应用程序窗口的主要容器,代表整个应用窗口。JPanel
是一个轻量级容器,用于组织和管理其他GUI组件,比如按钮、文本框等。JFrame
是顶层窗口,通常代表应用程序的主窗口。JPanel
是一个子容器,可以嵌套在JFrame
或其他JPanel
中,用于更灵活地组织UI布局。JFrame
通常是应用程序的整体窗口,而JPanel
通常用于划分和组织界面的不同部分。
关于具体的区别,我会单独出一期blog来为大家介绍,以便大家在后续项目的编写
当继承自JFrame,运行后,会出现“adding a window to a container”报错,
这是因为其中Window代表了一个顶级窗口,而Container是可以包含其他组件的对象,不应该将Window添加到Container中,而JFrame刚好继承自Window,JPanel刚好继承自Container,所以会产生报错,我们需要的是一个用于承载和组织UI组件的画板。
(2)关于setPreferredSize()函数的运用
在这个界面中,为了设定组件区域和下面的绘图区域的大小,我在这里运用了setPreferredSize()函数,它的作用是修改组件的大小,它接受一个 Dimension 对象作为参数,该对象定义了组件的宽度和高度。它可以配合setResizable(false)函数来使用,这个函数的作用为用户将无法通过鼠标拖拽边框来改变窗口的大小。
四、实现手写
这部分处理起来相对比较简单,代码量较少,但是有很多地方容易出错。完成手绘的代码将放在Listener中,手绘的动作由mouseDragged函数完成。此外,Listener中还有对按下按钮后的函数调用的控制。由于我想将 FileOp.java 中实现与文件操作相关的识别和训练的方法,所以在编写FileOp.java时候还需要传进Listener的参数,需要增加部分关联,这个先放一放。下面是实现的代码。
1、Listener类的实现
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class Listener extends MouseAdapter implements ActionListener {
private final Graphics2D g;
private final Main panel;
private final int[][] pixel = new int[40][40];
public Listener(Main panel) {
g = (Graphics2D) panel.getGraphics();
this.panel = panel;
}
/**
* Handles the action events triggered by the buttons.
*
* @param e the ActionEvent object representing the action event
*/
@Override
public void actionPerformed(ActionEvent e) {
// Checks if the action command is "识别" (Recognition)
if (e.getActionCommand().equals("识别")) {
// Performs recognition
FileOp.performRecognition();
// Repaints the panel
panel.repaint();
}
// Checks if the action command is "训练" (Training)
if (e.getActionCommand().equals("训练")) {
// Performs training
FileOp.performTraining();
// Repaints the panel
panel.repaint();
}
// Checks if the action command is "重置" (Reset)
if (e.getActionCommand().equals("重置")) {
// Resets the state
resetPixels();
// Repaints the panel
panel.repaint();
}
}
/**
* Resets the pixel array by setting all elements to zero.
*/
private void resetPixels() {
for (int i = 0; i < 40; i++) {
for (int j = 0; j < 40; j++) {
pixel[i][j] = 0;
}
}
System.out.println("重置");
}
/**
* Resets the pixels and prints a message when the mouse is pressed.
*
* @param e the MouseEvent object representing the mouse press event
*/
@Override
public void mousePressed(MouseEvent e) {
// Reset the pixels
resetPixels();
// Print a message
System.out.println("鼠标按下");
}
/**
* This method is called when the mouse is dragged.
*
* @param e The MouseEvent object containing information about the mouse event.
*/
@Override
public void mouseDragged(MouseEvent e) {
// Set the color to black
g.setColor(Color.BLACK);
// Get the x and y coordinates of the mouse event
int x = e.getX();
int y = e.getY();
// Fill a rectangle with the color black at the specified coordinates
g.fillRect(x, y, 10, 10);
// Update the pixel array to indicate that the pixels within the rectangle are filled
for (int i = x; i < x + 20; i++) {
for (int j = y; j < y + 20; j++) {
pixel[i / 10][j / 10] = 1;
}
}
}
/**
* Called when the mouse button is released.
*
* @param e the MouseEvent object containing information about the event
*/
@Override
public void mouseReleased(MouseEvent e) {
System.out.println("鼠标释放");
}
}
2、继承MouseAdapter的用途
这里我选择了继承MouseAdapter,它的作用是我可以选择性地重写你感兴趣的方法,而不需要实现接口中的所有方法。上述代码中,我已经重写了 mousePressed、mouseDragged 和 mouseReleased 方法,而忽略了 MouseListener 和 MouseMotionListener 提供的其他方法。这无疑提供了便利性。
3、探究repaint函数的作用
repaint()
方法是用于请求重新绘制组件的 Java Swing 方法。在上述代码中,repaint()
方法被用来更新用户界面的显示,特别是在执行了识别、训练或重置操作后。
在代码中的 actionPerformed()
方法中,每当点击了"识别"、"训练"或"重置"按钮时,相应的操作被执行完毕后,panel.repaint()
被调用。这会触发 panel
(即用户界面的主要绘图面板)进行重新绘制,以反映对应操作所带来的变化。
因此,repaint()
的作用是告诉 Swing 库需要重新绘制组件,以确保用户界面的更新和呈现能够反映最新的状态或更改。——GPT 3.5
以上是ChatGPT回答的内容,我觉得十分清晰(偷懒)。以后我会单独出一期,分析一下repaint函数的源代码。
总结
这篇文章是java手写识别项目的第一期,主要是大框架的搭建,没有涉及比较复杂的算法,但是有不少细节需要注意。在后续的文章中,我将深入探讨KNN算法的实现和其他文件的功能,会进一步丰富这个项目。