基于RSSI及KNN算法的WiFi室内定位实现
本文是基于RSSI和KNN算法室内定位的简单实现。信号采集使用WirelessMon,数据库使用MySQL。通过信号采集、数据预处理、构建指纹库、K临近算法位置信息匹配等过程实现了简单的室内定位。
目录
RSSI介绍
1、RSSI是一种基于测距的定位技术,其测距原理为接收机通过测量射频信号的能量来确定与发送机的距离。
(1)无线信号的发射功率和接收功率之间的关系如下图上式所示,P(R)是无线信号的接收功率,P(T)是无线信号的发射功率,r是收发单元之间的距离,n是传播因子,传播因子的数值取决于无线信号传播的环境。下式是接收信号功率转换为dBm的表达式,A可以看作信号传输1m时接收信号的功率。
(2)无线信号接收强度指示与信号传播距离之间的关系图,从理论曲线可以看出,无线信号在传播过程的近距离上信号衰减相当厉害,远距离时信号呈缓慢线性衰减。
2、该方法实现简答,因此被广泛应用,但使用时需要注意遮盖或者折射现象会引起接收端产生严重的测量误差,导致精度降低。
无线信号强度指纹库Fingerprint定位原理
1、 假设下图是某房间的俯视图,在房间的一角有一个WiFi设备,将房间分割成m行n列的一个网格,在每个网格线的交点上测得对应的无线信号强度,测得每个交点(下面称节点)的无线信号强度,存放在库中,由于该模型类似于人的指纹分布,因此称其为指纹库。
2、所有点的无线信号强度被测出来并组成一个指纹库后,理论上我们之后在所测位置测得无线信号强度后,就可以通过在指纹库中进行匹配得到所处的位置,达到定位的目的。
KNN算法原理
KNN,即K临近算法。KNN算法的原理很简单,以上述指纹库定位为例,当有一个新的无线信号强度x出现时(x不在指纹库中),那么仅依靠指纹库去匹配的到无线信号强度x所处的位置是不可行的。使用KNN临近算法,在一定限制范围内,指以x为中心的圆域内,让x去到指纹库中与满足条件(即在圆域内的数据)的筛选项进行比较,得出与x最接近的K个临近节点,例如K=3,那么就在该范围(圆域)内找出3个与x最为接近的值,在3个临近节点中,得到出现次数最多(>=2)的那个节点的位置,我们就可以认为是x对应的位置。以上描述比较抽象,可以参考下图进行理解:
黑色点为待测节点,蓝色虚线圈出的区域为圆域,K=3,即找与其最邻近的三个节点,由图看出,找出的3个临近位置节点中蓝色节点数量最多,那么信号强度x对应的位置就可以认为是蓝色节点对应的位置。
对于无线信号来说,其波动性比较大,因此在测量室内某个位置的信号强度时,通常会测量多次,通过一系列算法(例如加权平均、最简单的算数平均等)或进行数据分析得出一组相对准确的数据作为该节点的无线信号强度,即一个节点对应了一组由不同算法得出的相对较准确的信号强度。
基于RSSI及KNN算法的室内定位实现
以下是基于RSSI及KNN算法的简单室内定位的实现,使用Java语言和MySQL数据库,通过一系列的数据处理最终可以相对比较准确的实现室内定位。
需求分析及设计思路
需求分析
1、通过用户随着携带的终端设备采集室内无线信号强度信息,数据经预处理后建立信号强度和位置的指纹库。
2、终端设备实时采集到的信号强度数据和指纹库中的信息进行相关性匹配来完成定位,在部署实验场景采集数据的基础上,设计建立指纹库,并编程实现GUI界面,构建室内定位系统,要求能够快速且较准确实现用户室内定位。
设计思路
1、信号采集:在室内部署WiFi,使用WirelessMon软件(WirelessMon下载地址-官网)在室内不同位置进行无线信号强度的采集,并记录该位置信息和采集的一组无线信号强度值,在室内不同位置分别进行采集,最终得到室内不同位置无线信号强度的数据集。
2、将数据集导入数据库。
3、使用JDBC连接数据库并从数据库中将数据集提取出来放入二位数组中,便于后续对数据进行处理。
4、数据处理方法:算数平均法,中值法和加权移动平均算法。
5、分别将4中三种不同算法算出的无线信号强度以及对应的位置写回到数据库中新的表中,该表即构成了需求分析中所述的指纹库。
6、使用JDBC将新的数据表中的数据提取出来,使用KNN临近算法进行匹配完成定位。
7、GUI界面基于Java swing、awt框架实现。
数据库设计
1、存放原始数据集的数据表字段设计如下,字段含义见注释。
2、存放数据处理之后的数据表设计,即需求分析所述的指纹库。
Java实现
包括以下7个模块,图形化界面设计,按钮监听,数据库工具类,从数据库获取数据,数据预处理(算术平均、中值、加权移动平均),构建指纹库(将处理之后的数据写回到数据库中新的表中)和K临近算法(KNN)匹配定位。
图形化界面设计
ProFrame类,主要实现界面的设计。GUI通过Java swing和awt框架实现。通过JFrame类创建一个窗口,再向窗口里放置各类控件,实现一个简单的图形化界面。
如下图:
static JTextField jtf = new JTextField("");
static JTextArea jta = new JTextArea();
static JTextArea jta1 = new JTextArea();
public void initUI(){
JButton jb = new JButton("获取定位");
jb.setBounds(120, 70, 100, 30);
jb.addActionListener(new ButtonListener());
JLabel jl = new JLabel("请输入你所在位置的信号强度:");
jl.setBounds(30, 30, 200, 25);
JLabel jl1 = new JLabel("你所在的大致位置在:");
jl1.setBounds(30, 105, 200, 25);
JLabel jl2 = new JLabel("KNN算法返回的3个临近位置信息:");
jl2.setBounds(30, 205, 200, 25);
JLabel jl3 = new JLabel("dBm");
jl3.setBounds(270, 30, 30, 25);
jtf.setBounds(220, 30, 50, 25);
jta.setBounds(30, 130, 270, 60);
Font f = new Font("宋体", Font.BOLD, 14);
jta.setFont(f);
jta.setEditable(false);
jta1.setBounds(30, 230, 270, 60);
jta1.setFont(f);
jta1.setEditable(false);
JFrame jf = new JFrame("基于RSSI的室内定位");
jf.setBounds(480, 150, 360, 360);
jf.getContentPane().setBackground(Color.LIGHT_GRAY);
jf.setLayout(null);
jf.setVisible(true);
jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
jf.add(jb);
jf.add(jtf);
jf.add(jl);
jf.add(jl1);
jf.add(jl2);
jf.add(jl3);
jf.add(jta);
jf.add(jta1);
}
/**
* @return 输入信号强度的编辑框是否为空
*/
public static boolean jtf_isEmpty() {
String str = jtf.getText();
return str.equals("");
}
/**
* @return 输入无线信号强度编辑框中的值
*/
public static double jtf_content() {
return Double.parseDouble(jtf.getText());
}
按钮监听
ButtonListener类,该类实现按钮监听。判断信号强度输入编辑框内是否有数据,如果为空,提示用户先进行输入,不为空则根据get_location()方法返回的不同值进行判断,根据不同的返回值,做出相应的处理。
public class ButtonListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand().equals("获取定位")){
if (ProFrame.jtf_isEmpty()) {
JOptionPane.showMessageDialog(null, "请先输入无线信号强度!",
"提示", JOptionPane.ERROR_MESSAGE);
} else {
try {
//接收从get_location返回来的位置信息
String location = KNN.get_location();
if (location.equals("无法获取定位")) {
JOptionPane.showMessageDialog(null, "未在数据库中匹配到对应位置信息!",
"提示", JOptionPane.INFORMATION_MESSAGE);
ProFrame.jta.setText(null);
ProFrame.jta1.setText(null);
} else if (location.equals("位置不确定")) {
JOptionPane.showMessageDialog(null, "无法确定你所在的位置",
"提示", JOptionPane.INFORMATION_MESSAGE);
ProFrame.jta.setText(null);
} else
//将位置信息输出在只读编辑框jta中
ProFrame.jta.setText(location);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
}
数据库工具类
DBUtil类,通过MySQL提供的jar包(8.0.26版本),实现数据库的连接和释放数据库资源。该类实现DBConnInfo接口,接口封装了数据库连接需要使用的参数信息->username、password和url。
public class DBUtil implements DBConnInfo {
private static final String username = DBConnInfo.username;
private static final String password = DBConnInfo.password;
private static final String url = DBConnInfo.url;
//获取数据库连接
public static Connection getConnectDB() {
Connection conn = null;
try {
conn = DriverManager.getConnection(url, username, password);
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
//释放数据库资源
public static void closeDB(ResultSet rs, PreparedStatement stm, Connection conn) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (stm != null) {
try {
stm.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
从数据库中获取数据
GetDBData类,实现从数据库中的数据表(signal_dataset)中获取数据,并将拿到的数据放到一个二维数组中,后续通过该数组对数据进行处理,处理后的数据将作为指纹库的信息重新写回新的数据表构建指纹库。
public class GetDBData {
/**
* getData()方法:数据库的数据转换为二维数组并返回一个String类型的二维数组
* @return 数据库signal_dataset表中数据转换后的二维数组
* @throws SQLException 抛出连接数据库的异常
*/
public static String[][] getData() throws SQLException {
//获取连接
Connection conn = DBUtil.getConnectDB();
//创建sql语句,查询整张原始数据表signal_dataSet
String sql = "select * from signal_dataset";
//定义结果集
ResultSet rs;
//创建PreparedStatement对象,准备执行,即做好了执行sql前的准备
PreparedStatement p_stmt = conn.prepareStatement(sql);
//通过PreparedStatement对象的executeQuery方法执行sql语句
rs = p_stmt.executeQuery();
//定义变量记录signal_dataSet表中有多少行数据
int row = 0;
int col = 17;//signal_dataSet表中共有17列
while (rs.next()) {
//rs.next()返回值为boolean类型,判断是否还有下一行数据
row++;
}
//定义二维数组,存放数据表中的内容
String[][] dataSet = new String[row][col];
rs = p_stmt.executeQuery();//特别重要,不写改行代码取到数据的全是null,因为执行上面的while(rs.next())后,ResultSet对象的下标已指到0
//循环遍历将数据表中的数据存放到二维数组中
for (int i = 0; rs.next(); i++) { //当前光标所在行
for (int j = 0; j < col; j++) { //对于每一行,将对应列数据赋给数组,数据表中行和列均从1开始
dataSet[i][j] = rs.getString(j + 1);//通过列索引将数据库的每一列元素存入数组
}
}
DBUtil.closeDB(rs,p_stmt,conn);
//返回二维数组
return dataSet;
}
}
数据处理
无线信号波动性较大,会受到障碍物等外界环境的影响,因此需要对每个位置采集到的无线信号强度值进行处理。在实现过程中采用了三种比较简单的算法对数据进行处理。分别是算术平均法、中值法和加权移动平均算法,通过三个基础算法计算出每个位置对应的三个相对较准确的无线信号强度值,作为指纹库相关信息录入指纹库,用于后续KNN算法进行匹配定位。
数据处理-算术平均法
对每个位置对应的16个无线信号强度值取平均值,最后将计算出的数据放入一个一维数组中并返回,作为写回数据库locata_final表中RSSI_avg字段的数据。
/**
* getAvg():计算每个位置对应的一组数据的平均值并返回一个double数组
* @return 计算每个点对应的一组无线信号强度的算术平均值,作为KNN算法参考的一项数据
* @throws SQLException 抛出连接数据库的异常
*/
public static double[] getAvg() throws SQLException {
//定义二维数组接收getData返回的数据
String[][] data = GetDBData.getData();
//定义一维数组,存放每行数据的平均值,数组长度为二位数组的长度,即行数
double[] RSSI_avg = new double[data.length];
//定义每行数据的和sum
int sum = 0;
//循环遍历二位数组中的数据字段,即1-16列的数据
for (int i = 0; i < data.length; i++) {
for (int j = 1; j < data[0].length; j++) {
sum = sum + Integer.parseInt(data[i][j]);//data中存放的是String型,要先转换
}
RSSI_avg[i] = sum / 16.0;
sum = 0;
}
return RSSI_avg;
}
数据处理-中值法
中值法:将每行数据(除了第一列即位置对应的列)取出放入一个一维数组中进行排序,计算中值的时候没有考虑奇偶两种情况,因为已知原始数据表中一个位置对应16个无线信号强度,即偶数,因此直接取排序后数组中的中间两位取平均值即可。将每行数据对应的中值放入一维数组中,作为写回数据库locata_final表中RSSI_median字段的数据。
/**
* getMedian():计算每个位置对应一组数据的中位数并返回一个double数组
* @return 计算每个点对应的一组无线信号强度的中位数,作为KNN算法参考的一项数据
* @throws SQLException 抛出连接数据库的异常
*/
public static double[] getMedian() throws SQLException {
//定义二维数组接收getData返回的数据
String[][] data = GetDBData.getData();
//定义一维数组,存放每行数据的平均值,数组长度为二位数组的长度,即行数
double[] RSSI_median = new double[data.length];
for (int i = 0; i < data.length; i++) {
int[] array = new int[data[i].length - 1];
for (int j = 1; j < data[i].length; j++) {
array[j - 1] = Integer.parseInt(data[i][j]);//将二维数组的每行数据赋给一维数组
//调用Arrays类的sort方法进行排序(仅传入数组默认为升序排列)
Arrays.sort(array);
}
//这里不再判断数组长度是奇数还是偶数,已知长度为16,取中间两位求平均值最为中值存入RSSI_median数组中
RSSI_median[i] = (array[array.length / 2] + array[array.length / 2 - 1]) / 2.0;
}
//返回存放每组数据中值的数组
return RSSI_median;
}
数据处理-加权移动平均法
加权移动平均算法的原理:是对观察值分别给予不同的权数,按不同权数求得移动平均值,并以最后的移动平均值为基础,确定预测值的方法。
该算法的基本实现过程如下图所示:
/**
* getWeightMovingAvg():加权移动平均法,可以根据一组数据进行预测
* @return 每个位置对应一组数据得出的预测值封装的double数组
* @throws SQLException 抛出连接数据库的异常
*/
public static double[] getWeightMovingAvg() throws SQLException {
String[][] data = GetDBData.getData();//接收返回的二位数组
double[] RSSI_weightAvg = new double[data.length];//存放加权平均算法得出的数据
for (int i = 0; i < data.length; i++) {
double s;//定义一组数据的预测值
double[] array = new double[data[i].length - 1];//后面使用时类型需要是double型
for (int j = 1; j < data[i].length; j++) {
array[j - 1] = Integer.parseInt(data[i][j]);//将二维数组的每行数据赋给一维数组
}
//对于一行数据使用加权移动平均算法预测
int length = array.length;//数组的长度
//一直循环迭代,直到达到条件退出迭代
while (true) {
s = 0.0;
for (int k = 0; k < length; k++) {
s += (k + 1) * array[k];//s=1*array[0]+2*array[1]+3*array[2]...
}
s /= (length * (length + 1) / 2.0);//s=s/(1+2+3+4...),前面给每个数据加了系数,现在要除掉
for (int k = 0; k < length - 1; k++) {
array[k] = array[k + 1];//将后一个数赋给前一个数
}
array[length - 1] = s;//再把s赋给数组的最后一个数
double nd = array[array.length - 1];//获取数组的最后一个数,即刚刚被赋值的数
int js = 0;//用于计数数组中所有数据是否与
for (int k = 0; k < array.length - 1; k++) {//对于除了最后一个数的全部数据
//将除了数组中最后一个数(即上面求出的s),依次与数组的最后一个数比较,即s
if (nd == array[k]) {
js++;//如果相等,js++;即找数组中与数组中最后一个数一样的
}
}
//直到所有数据都一样,退出迭代
if (js == array.length - 1) {
break;
}
}
BigDecimal b = new BigDecimal(s);
//对预测值保留4位小数
RSSI_weightAvg[i] = b.setScale(4, RoundingMode.DOWN).doubleValue();
}
return RSSI_weightAvg;//返回加权平均算法预测每行数据对应的值double数组
}
构建指纹库
指纹库,即一个位置对应其无线信号强度值(映射)。将数据处理模块处理的三组数据作为指纹库的无线信号强度,与对应的位置共同组成指纹库,即指纹库中一个位置对应三个不同算法处理过后的无线信号强度。实现过程就是将位置、位置对应的无线信号强度的三种算法处理值写回数据库。
public class BuildFingerprint {
/**
* rewriteDB():将计算出的每个位置对应的三组数据以及对应的位置(二维数组的第一列)
* 写回到数据库表locate_final
* @throws SQLException 抛数据库异常
*/
public void rewriteDB() throws SQLException {
String[][] data = GetDBData.getData();//拿到存放初始数据信号数据的二维数组,写回到数据库需要用到对应位置信息
//以下三个数组分别接收计算出的平均值、中位数和加权平均算法的预测值
double[] RSSI_avg = DataHandle.getAvg();
double[] RSSI_median = DataHandle.getMedian();
double[] RSSI_weightAvg = DataHandle.getWeightMovingAvg();
//创建数据库连接对象
Connection conn = DBUtil.getConnectDB();
for (int i = 0; i < data.length; i++) {
//创建sql语句
String insert_sql = "insert into locate_final(location,RSSI_avg,RSSI_median,RSSI_weightAvg) " +
"values('" + data[i][0] + "','" + RSSI_avg[i] + "','" + RSSI_median[i] + "','" + RSSI_weightAvg[i] + "')";
Statement stmt = conn.createStatement();//创建statement对象
int count = stmt.executeUpdate(insert_sql);
//executeUpdate方法返回受影响的行数,因此判断count是否大于0就可以判断数据写回是否成功
if (count > 0)
System.out.println("数据插入成功!");
else
System.out.println("数据插入失败!");
}
}
}
KNN算法
KNN算法的原理见上文。实现过程即在一定范围(输入值-1->输入值+1)内找出与输入的无线信号强度最接近的三个值及这三个值对应的位置,出现次数最多的那个位置作为输入无线信号强度对应的位置,即作为定位信息输出。分为两步:
第一步:先找出在指定范围内的无线信号强度值,将所有满足条件的值与输入的无线信号强度值做差(目的是找出与输入无线信号强度值最接近的三个),将差值与对应位置放入Map集合中,其中key为差值,value为对应位置。
第二步:对Map集合的key进行排序,找出差值最小的三个(即与输入的信号强度最接近的三个),在通过<key,value>找出对应位置。对找出的三个位置进行分析,出现次数最多(>=2)那个位置的作为输入信号强度对应的位置。
/**
* KNN临近算法,输入无线信号强度,在生成的新的数据库中进行匹配
* K=3(参考邻居标签的个数),即寻找在一定范围内(输入值-1->输入值+1)与输入的无线信号强度最近的三个值
* 并匹配对应位置,出现次数最多的那个位置作为输出的位置
* 即如果某个位置出现在次数在两次或两次以上,就可以判断该位置为最终定位位置
* 如果出现了三个不同的位置或在规定范围内没有找出3个相邻的标签,则无法对该点进行定位,输出相关信息即可
*/
public class KNN {
public static String get_location() throws SQLException {
String location;//定义最终输出的位置
Connection conn = DBUtil.getConnectDB();
//获取编辑框的值
double inputRSSI = ProFrame.jtf_content();
String sql = "select * from locate_final";
ResultSet rs;
PreparedStatement p_stem = conn.prepareStatement(sql);
rs = p_stem.executeQuery();
int row = 0;
int col = 4;
while (rs.next()) {
row++;
}
//定义数组存放locate_final表的数据
String[][] data_final = new String[row][col];
rs = p_stem.executeQuery();
for (int i = 0; rs.next(); i++) {
for (int j = 0; j < col; j++) {
data_final[i][j] = rs.getString(j + 1);//将每一个数据放入二维数组中
}
}
//找出所有距离输入无线信号强度为1的数据,放入map中
//其中key为对应查找出的信号强度,因为最终要使用到信号强度在哪一行来判断对应信号的位置
//value为查找到的数据对应的位置
Map<Double, String> map = new HashMap<>();
for (int i = 0; i < data_final.length; i++) {
for (int j = 1; j < data_final[i].length; j++) {
if (Double.parseDouble(data_final[i][j]) <= inputRSSI + 1 &&
Double.parseDouble(data_final[i][j]) >= inputRSSI - 1) {
map.put(abs(Double.parseDouble(data_final[i][j]) - inputRSSI), data_final[i][0]);
}
}
}
if (map.size() < 3) { //如果在指定范围内找不到3个或3个以上的数据,则无法对该点位置做出判断
location = "无法获取定位";
} else { //如果找到了3个或3个以上的数据,找距离其最近的数3个
List<Double> list = new ArrayList<>(map.keySet());//对查找出的数据与输入信号强度做差的值放入集合中
Collections.sort(list);//对其进行排序
//取前三个,即与输入无线信号强度最接近的值,再通过查找对应的value,得出所在的位置作为候选定位位置
double data1 = list.get(0);
double data2 = list.get(1);
double data3 = list.get(2);
//再判断三个位置分别是什么,即取map中找对应的位置
String location1 = map.get(data1);
String location2 = map.get(data2);
String location3 = map.get(data3);
//将KNN算法得出的三个临近位置显示在界面
ProFrame.jta1.setText(location1 + "," + location2 + "," + location3);
//如果三个位置都相同,返回其中一个位置即可
if (location1.equals(location2) && location1.equals(location3)) {
location = location1;
} else if (location1.equals(location2) || location1.equals(location3)) {
//有两个位置相同的情况,1和3或1和2,返回位置1即可
location = location1;
} else if (location2.equals(location3)) {
//有两个位置相同的情况,2和3相同,返回位置2
location = location2;
} else {
//即三个位置都不相同
location = "位置不确定";
}
}
DBUtil.closeDB(rs,p_stem,conn);
return location;
}
}
程序运行demo
室内定位demo
更多推荐
所有评论(0)