26 Commits

Author SHA1 Message Date
quinn
8a5d5275c4 Improved the power of the scan matcher to better work with more cases. 2023-12-11 15:14:12 -05:00
Quinn
f7fd6fc6a8 Merge pull request #11 from Cynopolis/create-unit-tests-for-the-scan-matching-algorithm
Create unit tests for the scan matching algorithm
2023-12-09 17:17:47 -05:00
Quinn
64bf8769a1 Finished visualizer for ICP algorithm and confirmed it's working as intended. 2023-12-09 17:16:45 -05:00
Quinn
c340c02085 Created a visualizer for the ICP algorithm to figure out what's going wrong. 2023-12-09 15:29:51 -05:00
Quinn
dbb6b519e6 Undid stupid windows OS specific changes. 2023-12-01 13:46:01 -05:00
quinn
df57253287 Added some unit tests for the ScanMatcher and fixed some broken functionality. 2023-11-29 20:44:54 -05:00
Quinn
6a7d3eeffc Began writing tests for the scan matcher. 2023-11-26 15:34:24 -05:00
Quinn
f59b9b9094 Removed map.txt file because it changes too much. 2023-11-26 13:55:48 -05:00
Quinn
2eedfaccbc removed map.txt from the project because it messes up the diff. 2023-11-24 18:23:39 -05:00
Quinn
b09e34d6e9 Merge pull request #9 from Cynopolis/view-class-returns-ScanPoint-objects
Started converting the view class over to using ScanPoints
2023-11-24 18:16:40 -05:00
Quinn
9deba45afd Added proper javadoc comments to the View code. 2023-11-24 18:16:21 -05:00
Quinn
a0173b1053 Started converting the view class over to ScanPoints. 2023-11-24 18:06:35 -05:00
Quinn
0c59839dfa Cleaned up scan matching implimentation. 2023-11-24 17:09:00 -05:00
Quinn
36a6c2267b basic outline for scan matching has been added. 2023-11-22 19:05:27 -05:00
Quinn
b505524fe1 Began implimenting scan graph 2023-11-22 18:44:34 -05:00
Quinn
7dc679371a Creating scan graph. 2023-11-21 21:30:21 -05:00
Quinn
bda601d326 Further tweaks to RANSAC algorithm and line of best fit 2023-05-03 16:22:38 -05:00
Quinn
62e7232435 Tweaking SLAM parameters to get better accuracy 2023-05-03 00:12:17 -05:00
Quinn
7298f80d36 First semi-successful test of feature extraction. I think the algorithm for line of best fit needs some work. Specifically, ifnding the starting and end point for the line. 2023-05-02 23:51:59 -05:00
Quinn
2f46605837 Added a saving and loading system for graph maps 2023-05-02 12:10:58 -05:00
Quinn
38847abead Added interactivity and can now save the graph state to a text file 2023-05-02 11:19:39 -05:00
Quinn
1a4d6e6909 Everything can now be drawn 2023-05-02 10:28:06 -05:00
Quinn
3321e77061 Added a Graph datastructure and refactored the Line/Vertex classes 2023-05-02 09:49:38 -05:00
Quinn
9b0acbe34c Refactored the ray class to have its interesections work more consistently. 2023-04-30 19:17:41 -05:00
Quinn
527615220f The ray class is mostly refactored, but the collision code needs to be debugged. 2023-04-08 10:18:56 -05:00
Quinn
cd4ec2c819 Refactoring the ray class to extend a line class 2023-04-06 18:05:56 -05:00
50 changed files with 2284 additions and 402 deletions

5
.gitignore vendored
View File

@@ -26,4 +26,7 @@ bin/
.vscode/ .vscode/
### Mac OS ### ### Mac OS ###
.DS_Store .DS_Store
map.txt
processing-4.3/

View File

@@ -0,0 +1,8 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="JavadocDeclaration" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ADDITIONAL_TAGS" value="brief" />
</inspection_tool>
</profile>
</component>

13
.idea/libraries/ejml.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<component name="libraryTable">
<library name="ejml">
<CLASSES>
<root url="file://$PROJECT_DIR$/lib/ejml-v0.42-libs" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="file://$PROJECT_DIR$/lib/ejml-v0.42-libs" />
</SOURCES>
<jarDirectory url="file://$PROJECT_DIR$/lib/ejml-v0.42-libs" recursive="false" />
<jarDirectory url="file://$PROJECT_DIR$/lib/ejml-v0.42-libs" recursive="false" type="SOURCES" />
</library>
</component>

1
.idea/misc.xml generated
View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_20" default="true" project-jdk-name="openjdk-20" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_20" default="true" project-jdk-name="openjdk-20" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />

124
.idea/uiDesigner.xml generated Normal file
View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

View File

@@ -1,2 +0,0 @@
# SLAM-Sim
SImulate SLAM algorithm using java and processing for visualizaiton.

View File

@@ -43,5 +43,9 @@
<SOURCES /> <SOURCES />
</library> </library>
</orderEntry> </orderEntry>
<orderEntry type="library" exported="" name="ejml" level="project" />
</component> </component>
</module> </module>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -3,6 +3,9 @@ import java.util.ArrayList;
import static java.lang.Math.PI; import static java.lang.Math.PI;
import static processing.core.PApplet.degrees; import static processing.core.PApplet.degrees;
import static processing.core.PApplet.radians; import static processing.core.PApplet.radians;
import Graph.*;
import Vector.Vector;
import processing.core.PApplet; import processing.core.PApplet;
public class Car{ public class Car{
@@ -35,22 +38,21 @@ public class Car{
} }
//draw the car and its views //draw the car and its views
public void drawCar(ArrayList<Wall> walls){ public void drawCar(PointGraph map, boolean SLAMIsHidden){
proc.stroke(255); proc.stroke(255);
proc.ellipse(pose.x, pose.y, carWidth, carLength); proc.ellipse(pose.x, pose.y, carWidth, carLength);
this.updateScan(walls); this.updateScan(map);
this.slam.drawLines(); if(!SLAMIsHidden){
this.slam.drawFeatures(proc);
}
} }
//With all the views that the car has, get their point list //With all the views that the car has, get their point list
void updateScan(ArrayList<Wall> walls){ void updateScan(PointGraph map){
for(View view : views){ for(View view : views){
view.look(walls); view.calculatePointScan(map);
} slam.RANSAC(view);
for(View view : views){
ArrayList<Vector> pointList = view.getPoints();
slam.RANSAC(pointList, view.getFOV() / view.getRayNum());
} }
} }

59
src/Graph/Edge.java Normal file
View File

@@ -0,0 +1,59 @@
package Graph;
public class Edge {
Vertex vStart = null;
Vertex vEnd = null;
float weight = 0;
/**
* @param vStart the vertex the edge starts at
* @param vEnd the vertex the edge ends at
* @param weight the weight of the edge
*/
Edge(Vertex vStart, Vertex vEnd, float weight){
this.vStart = vStart;
this.vEnd = vEnd;
this.weight = weight;
}
/**
* @param vStart the vertex the edge starts at
* @param vEnd the vertex the edge ends at
*/
protected Edge(Vertex vStart, Vertex vEnd){
this.vStart = vStart;
this.vEnd = vEnd;
}
Edge(){}
/**
* @return the weight of the edge
*/
public float getWeight(){
return weight;
}
/**
* @param newWeight set the edge's weight to something new
*/
public void setWeight(float newWeight){
this.weight = newWeight;
}
/**
* @return the vertex at the end of the edge
*/
public Vertex getEndVertex(){
return vEnd;
}
/**
* @return the vertex at the start of the edge
*/
public Vertex getStartVertex(){
return vStart;
}
}

225
src/Graph/Graph.java Normal file
View File

@@ -0,0 +1,225 @@
package Graph;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
public class Graph {
/**
* Create a new empty graph
*/
public Graph(){
// hash maps require an initial size to get started
adjList = new HashMap<Vertex, LinkedList<Edge>>(1024);
}
/**
* @return the total number of vertices in the graph
*/
public int numVertices(){
return adjList.size();
}
/**
* @return the total number of edges in the graph.
*/
public int numEdges(){
int sum = 0;
for(Vertex v : adjList.keySet()){
sum += adjList.get(v).size();
}
return sum;
}
/**
* @param v add the vertex v to the graph
*/
public void addVertex(Vertex v){
// check if it's already in the adjacency listVertex
if(adjList.containsKey(v)){
return;
}
adjList.put(v, new LinkedList<Edge>());
}
/**
* @param vStart the starting vertex
* @param vEnd the ending vertex
*/
public void addEdge(Vertex vStart, Vertex vEnd){
// don't add the edge if it is already added
for(Edge e : adjList.get(vStart)){
if(e.getEndVertex() == vEnd){
return;
}
}
adjList.get(vStart).add(new Edge(vStart, vEnd));
}
/**
* @brief Gets how many other vertices a given vertex points to
* @param v the vertex you want to know about
* @return the number of vertices this vertex points to
*/
public int outDegree(Vertex v){
return adjList.get(v).size();
}
/**
* @param v the vertex of interest
* @return the number of edges going into the vertex
*/
public int inDegree(Vertex v){
int count = 0;
for(Vertex v1 : adjList.keySet()){
for(Edge edge : adjList.get(v1)){
if(edge.getEndVertex() == v){
count++;
}
}
}
return count;
}
/**
* Print out all vertexes in the graph
*/
public void print(){
for(Vertex key : adjList.keySet()){
System.out.print("Key: " + key.getLabel() + ": ");
for(Edge edge : adjList.get(key)){
System.out.print(edge.getEndVertex().getLabel());
System.out.print(",");
}
System.out.println("NULL");
}
}
/**
* @param v the vertext to start the traverse at
* @return a list of vertexes in order of when they were visited from first to last.
*/
public ArrayList<Vertex> depthFirstTraverse(Vertex v){
ArrayList<Vertex> vertexList = new ArrayList<Vertex>();
DFSTraverse(v, vertexList);
resetVisits();
return vertexList;
}
/**
* @param v the vertex to begin the traverse at
* @param vertexList the vertex list to append visited vertexes to
*/
private void DFSTraverse(Vertex v, ArrayList<Vertex> vertexList){
vertexList.add(v);
v.setVisitedStatus(true);
LinkedList<Edge> edgeList = adjList.get(v);
for(Edge edge : edgeList){
if(!(edge.getEndVertex().visitedStatus())){
DFSTraverse(edge.getEndVertex(), vertexList);
}
}
}
/**
* @post all vertexes in the graph will be set to unvisited
*/
private void resetVisits(){
for(Vertex v : adjList.keySet()){
v.setVisitedStatus(false);
}
}
/**
* @param v the vertex that you wish to see the neighbors of
* @return a list of vertexes that are connected to v with ingoing or outgoing edges
*/
public ArrayList<Vertex> getNeightborVerts(Vertex v){
// iterate through all of the vertexes and make sure they're marked as unvisited
for(Vertex v1 : adjList.keySet()){
v1.setVisitedStatus(false);
}
ArrayList<Vertex> neighbors = new ArrayList<>();
for(Vertex v1 : adjList.keySet()){
for(Edge edge : adjList.get(v1)){
if(edge.getStartVertex() == v && !edge.getEndVertex().visitedStatus()){
edge.getEndVertex().setVisitedStatus(true);
neighbors.add(edge.getEndVertex());
}
if(edge.getEndVertex() == v && !edge.getStartVertex().visitedStatus()){
edge.getStartVertex().setVisitedStatus(true);
neighbors.add(edge.getStartVertex());
}
}
}
resetVisits();
return neighbors;
}
/**
* @param v the vertex that you wish to start the search at
* @return A list of vertexes in order of visitation where the search goes wide and then deep
*/
public ArrayList<Vertex> breadthFirstSearch(Vertex v){
ArrayList<Vertex> queue = new ArrayList<>();
ArrayList<Vertex> outputList = new ArrayList<>();
outputList.add(v);
queue.add(v);
v.setVisitedStatus(true);
while(queue.size() > 0){
Vertex next = queue.get(0);
queue.remove(0);
LinkedList<Edge> edgeList = adjList.get(next);
for(Edge edge : edgeList){
if(!edge.getEndVertex().visitedStatus()){
queue.add(edge.getEndVertex());
outputList.add(edge.getEndVertex());
edge.getEndVertex().setVisitedStatus(true);
}
}
}
resetVisits();
return outputList;
}
/**
* @brief remove the given vertex and all edges that reference it from the graph
* @param v the vertex to remove from the graph.
*/
public void removeVertex(Vertex v){
// find all edges that point to the removed vertex
ArrayList<Edge> edgesToRemove = new ArrayList<>();
ArrayList<Vertex> startVertex = new ArrayList<>();
for(Vertex v1 : adjList.keySet()){
for(Edge e : adjList.get(v1)){
if(e.getEndVertex() == v){
edgesToRemove.add(e);
startVertex.add(e.getStartVertex());
}
}
}
// remove all of those edges from the adjList
int i = 0;
for(Edge e : edgesToRemove){
adjList.get(startVertex.get(i)).remove(e);
i++;
}
// remove the vertex from the adjacency list
adjList.remove(v);
}
protected HashMap<Vertex, LinkedList<Edge>> adjList;
}

53
src/Graph/LineEdge.java Normal file
View File

@@ -0,0 +1,53 @@
package Graph;
import Vector.*;
import processing.core.PApplet;
import java.awt.*;
import static java.lang.Math.PI;
public class LineEdge extends Edge implements LineInterface{
protected PointVertex vStart;
protected PointVertex vEnd;
protected Line line;
public LineEdge(PointVertex vStart, PointVertex vEnd) {
super(vStart, vEnd);
this.line = new Line(vStart.getPos(), vEnd.getPos());
}
public Vector getDirection(){
return line.getDirection();
}
public Vector getPosition(){
return line.getPosition();
}
public float getLength(){
return line.getLength();
}
public float getAngle(){
return line.getAngle();
}
public Vector endPoint(){
return line.endPoint();
}
public float getDistance(Vector point){
return line.getDistance(point);
}
public void draw(PApplet proc){
line.draw(proc);
Vector leftFlange = line.getDirection().rotate2D((float)(-3*PI/4)).normalize().mul(20);
Vector rightFlange = line.getDirection().rotate2D((float) (3*PI/4)).normalize().mul(20);
Line l1 = new Line(line.endPoint(), line.endPoint().add(leftFlange));
Line l2 = new Line(line.endPoint(), line.endPoint().add(rightFlange));
l1.draw(proc);
l2.draw(proc);
}
}

152
src/Graph/PointGraph.java Normal file
View File

@@ -0,0 +1,152 @@
package Graph;
import Vector.Vector;
import processing.core.PApplet;
import java.io.File; // Import the File class
import java.io.FileWriter;
import java.io.IOException; // Import the IOException class to handle errors
import static java.lang.Math.pow;
import static java.lang.Math.sqrt;
import java.util.ArrayList;
public class PointGraph extends Graph {
PointVertex selectedVertex;
public PointGraph(){
super();
}
public void draw(PApplet proc){
for(Vertex v : adjList.keySet()){
PointVertex v1 = (PointVertex) v;
v1.draw(proc);
for(Edge e : adjList.get(v)){
LineEdge e1 = (LineEdge) e;
e1.draw(proc);
}
}
}
public void addEdge(PointVertex vStart, PointVertex vEnd){
addVertex(vStart);
// don't add the edge if it is already added
for(Edge e : adjList.get(vStart)){
if(e.getEndVertex() == (Vertex) vEnd){
return;
}
}
addVertex(vEnd);
LineEdge edge = new LineEdge(vStart, vEnd);
adjList.get((Vertex) vStart).add(new LineEdge(vStart, vEnd));
}
/**
* @param v set this vertex as the selected vertex in the graph
*/
public void setSelectedVertex(PointVertex v){
if(selectedVertex != null){
deselectVertex();
}
selectedVertex = v;
selectedVertex.setColor(new int[]{255, 0, 255, 0});
}
/**
* @pre a vertex needs to be selected
* @return the current pointvertex which is selected in the graph
*/
public PointVertex getSelectedVertex(){
if(selectedVertex == null){
if(super.adjList.size() > 0){
selectedVertex = (PointVertex)super.adjList.keySet().iterator().next();
}
else{
throw new NullPointerException();
}
}
return selectedVertex;
}
/**
* @return true if a vertex is currently selected in the graph
*/
public boolean vertexIsSelected(){
return selectedVertex != null;
}
/**
* @brief deselect the currently selected vertex
*/
public void deselectVertex(){
if(selectedVertex == null){
return;
}
selectedVertex.setColor(new int[]{127, 255, 0, 0});
selectedVertex = null;
}
/**
* @param x the x coordinate to search by
* @param y the y coordinate to search by
* @return the pointvertex closest to the given x, y coordinates
*/
public PointVertex getClosestVertex(Vector point){
if(super.adjList.size() == 0){
// TODO: choose a better exception name
throw new NullPointerException();
}
PointVertex closestVertex = (PointVertex) super.adjList.keySet().iterator().next();
float closestDist = -1;
for(Vertex v : super.adjList.keySet()){
PointVertex v1 = (PointVertex) v;
Vector p2 = v1.getPos();
float dist = p2.sub(point).mag();
if(dist < closestDist || closestDist == -1){
closestDist = dist;
closestVertex = v1;
}
}
return closestVertex;
}
public void removeVertex(PointVertex v){
if(selectedVertex == v){
selectedVertex = null;
}
super.removeVertex(v);
}
/**
* @return all edges in the graph
*/
public ArrayList<LineEdge> getAllEdges(){
ArrayList<LineEdge> edges = new ArrayList<>();
for(Vertex v : adjList.keySet()){
for(Edge e : adjList.get(v)){
LineEdge e1 = (LineEdge) e;
edges.add(e1);
}
}
return edges;
}
/**
* @return all vertexes in the graph
*/
public ArrayList<PointVertex> getAllVertexes(){
ArrayList<PointVertex> points = new ArrayList<>();
for(Vertex v : adjList.keySet()){
PointVertex v1 = (PointVertex) v;
points.add(v1);
}
return points;
}
}

View File

@@ -0,0 +1,122 @@
package Graph;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Scanner;
public class PointGraphWriter {
public void save(String filename, PointGraph g) throws IOException {
FileWriter file = new FileWriter(filename);
file.write("numVerts," + g.numVertices());
file.write("\nnumEdges," + g.numEdges());
// turn the hash map into something linear
ArrayList<PointVertex> verts = g.getAllVertexes();
ArrayList<LineEdge> edges = g.getAllEdges();
// save the vertexes
int countVerts = 0;
for(PointVertex v : verts){
// save the vertex position
file.write("\nvert,"+v.getPos().x+","+v.getPos().y);
countVerts++;
}
// save the edges
for(Edge e : edges){
int idx = 0;
file.write("\nedge,");
boolean otherIsWritten = false;
for(PointVertex v : verts){
if(e.getStartVertex() == (Vertex)v){
file.write("start,"+ idx);
if(!otherIsWritten){
file.write(",");
otherIsWritten = true;
}
}
else if(e.getEndVertex() == (Vertex)v){
file.write("end," + idx);
if(!otherIsWritten){
file.write(",");
otherIsWritten = true;
}
}
idx++;
}
}
file.close();
}
public PointGraph loadFile(String filename) throws NumberFormatException {
PointGraph g = new PointGraph();
File file = new File(filename);
Scanner reader;
try {
reader = new Scanner(file);
}
catch (FileNotFoundException e){
System.out.println("File not found");
return g;
}
ArrayList<PointVertex> vertices = new ArrayList<>();
while(reader.hasNextLine()){
String line = reader.nextLine();
ArrayList<String> args = parseLine(line);
String key = args.get(0);
switch (key) {
case "numVerts" -> System.out.println("Number of Vertexes: " + Integer.parseInt(args.get(1)));
case "numEdges" -> System.out.println("Number of Edges: " + Integer.parseInt(args.get(1)));
case "vert" -> {
float x = Float.parseFloat(args.get(1));
float y = Float.parseFloat(args.get(2));
PointVertex v = new PointVertex(x, y);
g.addVertex(v);
vertices.add(v);
}
case "edge" -> {
int startIdx;
int endIdx;
if (args.get(1).contains("start")) {
startIdx = Integer.parseInt(args.get(2));
endIdx = Integer.parseInt(args.get(4));
}
else{
startIdx = Integer.parseInt(args.get(4));
endIdx = Integer.parseInt(args.get(2));
}
g.addEdge(vertices.get(startIdx), vertices.get(endIdx));
}
default -> System.out.println("Unrecognized Line: " + line);
}
}
return g;
}
private ArrayList<String> parseLine(String line){
ArrayList<String> args = new ArrayList<>();
StringBuilder arg = new StringBuilder();
for(char letter : line.toCharArray()){
if(letter == ','){
args.add(arg.toString());
arg = new StringBuilder();
continue;
}
if(letter == '\n'){
args.add(arg.toString());
break;
}
arg.append(letter);
}
if(!args.get(args.size()-1).contains(arg.toString())){
args.add(arg.toString());
}
return args;
}
}

View File

@@ -0,0 +1,66 @@
package Graph;
import Vector.Vector;
import processing.core.PApplet;
public class PointVertex extends Vertex {
private Vector position;
private int[] color = new int[]{127, 255, 0, 0};
/**
* @param xPos the x position of the vertex
* @param yPos the y posiiton of the vertex
*/
public PointVertex(float xPos, float yPos){
this.position = new Vector(xPos, yPos);
}
public PointVertex(Vector position){
super();
this.position = position;
}
/**
* @param xPos the x position of the vertex
* @param yPos the y posiiton of the vertex
* @param label the label of the vertex
*/
PointVertex(float xPos, float yPos, String label){
super(label);
this.position = new Vector(xPos, yPos);
}
/**
* @param x the new x position of the vertex
* @param y the new y posiiton of the vertex
*/
public void setPos(float x, float y){
this.position = new Vector(x, y);
}
/**
* @return a two eleement float array containing the x and y coordinates of the vertex respectively.
*/
public Vector getPos(){
return position;
}
/**
* @param newColor a 4 element int array containing th alpha, r, g ,and b components respectively
*/
public void setColor(int[] newColor){
this.color = newColor;
}
/**
* @return a 4 element int array containing th alpha, r, g ,and b components respectively
*/
public int[] getColor(){
return color;
}
public void draw(PApplet proc){
proc.stroke(color[1], color[2], color[3], color[0]);
proc.circle(position.x, position.y, 20);
}
}

45
src/Graph/Vertex.java Normal file
View File

@@ -0,0 +1,45 @@
package Graph;
public class Vertex {
private String vertexLabel;
private boolean isVisitedStatus = false;
/**
* Create a new vertex with a default label
*/
public Vertex(){
this.vertexLabel = "Unassigned";
}
/**
* @param label create a new vertex with this label
*/
public Vertex(String label){
this.vertexLabel = label;
}
/**
* @return the label of the vertex
*/
public String getLabel(){
return vertexLabel;
}
public void setLabel(String newLabel){
vertexLabel = newLabel;
}
/**
* @return if the node has been visited or not
*/
public boolean visitedStatus(){
return this.isVisitedStatus;
}
/**
* @param status set the visited status to true or false
*/
public void setVisitedStatus(boolean status){
this.isVisitedStatus = status;
}
}

View File

@@ -1,88 +0,0 @@
import processing.core.PApplet;
import java.util.List;
import static processing.core.PApplet.*;
public class Line{
private Vector direction = new Vector(0,0);
private Vector position = new Vector(0,0);
Line(Vector startPosition, Vector endPosition){
direction = endPosition.sub(startPosition);
position = startPosition;
}
/**
* attempt to find the line of best fit for the given points
* @param points the points to get the line of best for
*/
Line(List<Vector> points){
bestFit(points);
}
// least squares line of best fit algorithm
private void bestFit(List<Vector> points){
// get the mean of all the points
Vector mean = new Vector();
for(Vector point : points){
mean = mean.add(point);
}
mean = mean.div(points.size());
// this section calculates the direction vector of the line of best fit
Vector direction = new Vector();
float length = 1;
// get the rise and run of the line of best fit
for(Vector point : points){
direction.y += (point.x - mean.x)*(point.y - mean.y); // rise
direction.x += pow((point.x - mean.x),2);
// find the point that's furthest from the mean and use it to set the line length.
float dist = abs(point.sub(mean).mag());
if(dist > length){
length = 2*dist;
}
}
if(direction.y == 0){
this.direction = new Vector(0, 1);
}
else{
this.direction = new Vector(1, direction.y/direction.x);
}
this.direction = this.direction.normalize().mul(length);
this.position = mean.sub(this.direction.div(2));
}
public Vector getSlopeIntForm(){
float slope = direction.y / direction.x;
float intercept = position.y - slope * position.x;
return new Vector(slope, intercept);
}
public Vector getDirection(){
return direction;
}
public Vector getPosition(){
return position;
}
public float getLength(){
return direction.mag();
}
public void draw(PApplet screen){
Vector endPoint = this.position.add(this.direction);
screen.line(position.x, position.y, endPoint.x, endPoint.y);
}
/**
* @param point
* @return the smallest distance from the point to this line
*/
public float getDistance(Vector point){
return (point.sub(position).cross(direction)).mag() / direction.mag();
}
}

View File

@@ -1,14 +1,20 @@
import Graph.*;
import Vector.Vector;
import processing.core.PApplet; import processing.core.PApplet;
import java.util.ArrayList; import java.io.FileNotFoundException;
import java.io.IOException;
public class Processing extends PApplet { public class Processing extends PApplet {
Car car; Car car;
ArrayList<Wall> objects = new ArrayList<>();
public static PApplet processing; public static PApplet processing;
PointGraph map = new PointGraph();
boolean mapIsHidden = false;
boolean SLAMIsHidden = false;
public static void main(String[] args) { public static void main(String[] args) {
PApplet.main("Processing"); PApplet.main("Processing");
} }
@@ -17,33 +23,37 @@ public class Processing extends PApplet {
processing = this; processing = this;
car = new Car(processing, 100,100,50,40); car = new Car(processing, 100,100,50,40);
size(1000, 1000); size(1000, 1000);
car.addView(180,180); car.addView(90,180);
for(int i = 0; i < 20; i++){
Wall wall = new Wall(processing, new Vector((int)random(40, 1840), (int)random(40, 960)), (int)random(360), (int)random(100, 1000)); // for(int i = 0; i < 10; i++){
objects.add(wall); // PointVertex vStart = new PointVertex(random(50, 950), random(50, 950));
} // PointVertex vEnd = new PointVertex(random(50, 950), random(50, 950));
// map.addEdge(vStart, vEnd);
// }
} }
public void draw(){ public void draw(){
background(0); background(0);
// for(Wall object : objects){ if(!mapIsHidden){
// object.drawWall(); map.draw(processing);
// } }
car.drawCar(objects); car.drawCar(map, SLAMIsHidden);
//car.drive(new int[] {0, 0}); strokeWeight(2);
stroke(255);
} }
public void keyPressed(){ public void keyPressed(){
if(key == 'd'){ if(key == 'd'){
car.setPose(car.getPose().add(1, 0)); car.setPose(car.getPose().add(10, 0));
} }
if(key == 'w'){ if(key == 'w'){
car.setPose(car.getPose().add(0, -1)); car.setPose(car.getPose().add(0, -10));
} }
if(key == 'a'){ if(key == 'a'){
car.setPose(car.getPose().add(-1, 0)); car.setPose(car.getPose().add(-10, 0));
} }
if(key == 's'){ if(key == 's'){
car.setPose(car.getPose().add(0, 1)); car.setPose(car.getPose().add(0, 10));
} }
if(key == 'q'){ if(key == 'q'){
car.setAngle(car.getAngle()+1); car.setAngle(car.getAngle()+1);
@@ -51,9 +61,64 @@ public class Processing extends PApplet {
if(key == 'e'){ if(key == 'e'){
car.setAngle(car.getAngle()-1); car.setAngle(car.getAngle()-1);
} }
if(key == DELETE && map.vertexIsSelected()){
map.removeVertex(map.getSelectedVertex());
}
if(key == ' ' && map.vertexIsSelected()){
map.deselectVertex();
}
if(key == ESC){
System.out.println("Attempting to save map to file.");
try{
PointGraphWriter writer = new PointGraphWriter();
writer.save("map.txt", map);
}
catch(IOException e){
e.printStackTrace();
}
}
if(key == 'l'){
System.out.println("Attempting to load a map from file");
PointGraphWriter writer = new PointGraphWriter();
try {
map = writer.loadFile("map.txt");
} catch (NumberFormatException e) {
System.out.println("Number format incorrect");
e.printStackTrace();
}
}
if(key == 'h'){
mapIsHidden = !mapIsHidden;
}
if(key == 'j'){
SLAMIsHidden = !SLAMIsHidden;
}
} }
public void mousePressed(){
Vector clickPosition = new Vector(mouseX, mouseY);
if(map.numVertices() == 0){
PointVertex v = new PointVertex(clickPosition);
map.addVertex(v);
return;
}
PointVertex closestVertex = map.getClosestVertex(clickPosition);
float distance = closestVertex.getPos().sub(clickPosition).mag();
if(distance < 15){
if(map.vertexIsSelected()){
if(map.getSelectedVertex() == closestVertex){
map.deselectVertex();
}
else{
map.addEdge(map.getSelectedVertex(), closestVertex);
}
return;
}
map.setSelectedVertex(closestVertex);
return;
}
PointVertex v = new PointVertex(clickPosition);
map.addVertex(v);
}
} }

97
src/Ray.java Normal file
View File

@@ -0,0 +1,97 @@
import Graph.*;
import Vector.*;
import processing.core.PApplet;
import java.util.ArrayList;
import static processing.core.PApplet.*;
public class Ray extends Line {
float maxRayDistance = 1000;
int[] color = new int[]{255, 255, 255};
//takes the starting position of the ray, the length of the ray, and it's casting angle (radians)
Ray(Vector startPosition, float angle){
super(startPosition, startPosition.add(new Vector(cos(angle), sin(angle))));
direction = direction.mul(maxRayDistance);
}
public void drawRay(PApplet proc){
proc.stroke(color[0], color[1], color[2]);
proc.line(position.x, position.y, position.x + direction.x, position.y + direction.y);
// proc.noFill();
// proc.circle(position.x, position.y, 2*direction.mag());
// proc.fill(255);
}
//checks to see at what coordinate the ray will collide with an object and sets the ray length to meet that point.
public void castRay(PointGraph map){
float shortestWallDistance = maxRayDistance;
ArrayList<LineEdge> walls = map.getAllEdges();
for(LineEdge wall : walls){
// get the necessary vectors for two parameterized lines
// parameterized lines are of the form L = d*t + p
Vector d1 = this.direction.normalize().mul(maxRayDistance);
Vector d2 = wall.getDirection();
Vector p1 = this.position;
Vector p2 = wall.getPosition();
// calculate the parameters for the intersection t and u
float t = -(d2.x*(p2.y-p1.y) + d2.y*(p1.x-p2.x))/(d1.x*d2.y - d2.x*d1.y);
float u = -(d1.x*(p2.y-p1.y) + d1.y*(p1.x-p2.x))/(d1.x*d2.y-d2.x*d1.y);
// the lines will only be intersecting when both t and u are between 0 and 1.
if(!(0 <= t && t <= 1 && 0 <= u && u <= 1)){
continue;
}
// if the distance from the ray to the intersection is shorter than the shortestWallDistance, this is our new closest wall
float distance = d1.mul(t).add(p1).sub(this.position).mag();
if(distance < shortestWallDistance){
shortestWallDistance = distance;
}
}
// if we collided with a wall, set the ray's length to the distance from it to the collision
if(shortestWallDistance != maxRayDistance){
this.direction = this.direction.normalize().mul(shortestWallDistance);
}
else{
this.direction = this.direction.normalize().mul(maxRayDistance);
}
}
public Vector getPos(){ return this.position;}
public float getRayLength(){return this.direction.mag();}
public boolean hasCollided(){
return abs(this.direction.mag() - maxRayDistance) > 0.001;
}
//returns the absolute position of the point
public Vector getPoint(){
if(this.direction.mag() == 0){
return this.position;
}
return this.position.add(this.direction);
}
public void setPos(Vector newPosition){
this.position = newPosition;
}
public void setRayLength(int rayLength){this.direction = this.direction.normalize().mul(rayLength);}
public void setAngle(float angle){
float currentAngle = direction.angle();
this.direction = direction.rotate2D(angle - currentAngle);
}
}

View File

@@ -1,13 +1,16 @@
import Vector.*;
import processing.core.*; import processing.core.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import static java.lang.Math.random;
import static processing.core.PApplet.*; import static processing.core.PApplet.*;
public class SLAM{ public class SLAM{
ArrayList<Line> lines = new ArrayList<>(); ArrayList<Line> lines = new ArrayList<>();
ShortTermMem unassociatedPoints = new ShortTermMem();
private static PApplet proc; private static PApplet proc;
SLAM(PApplet processing){ SLAM(PApplet processing){
@@ -16,38 +19,41 @@ public class SLAM{
/** /**
* @param set the set to take a sub sample of * @param set the set to take a sub sample of
* @param indexRange the range within to take the sub sample
* @param subSampleSize the size of the sub sample * @param subSampleSize the size of the sub sample
* @return A random subset of the set within an indexRange and of size: subSampleSize * @param minAngle the minimum angle allowed in the subset
* @param maxAngle the maximum angle allowed in the subset
* @return A random subset of the set within the angle range
*/ */
private List<Vector> randomSample(ArrayList<Vector> set, int indexRange, int subSampleSize){ private List<Vector> randomSampleInAngleRange(ArrayList<Vector> set, int subSampleSize, float minAngle, float maxAngle){
// select a random laser data reading
int randomIdx = (int) proc.random(set.size() - 1); // index of starter reading
Vector point = set.get(randomIdx); // point of starter reading
// get a random sample of size numSampleReadings within degreeRange degrees of this laser reading. // create an arraylist with all points within the angle range from the given set
List<Vector> subSample; ArrayList<Vector> pointsInAngleRange = new ArrayList<>();
int rangeStart = randomIdx - indexRange >= 0 ? randomIdx - indexRange : 0; for(Vector point : set){
int rangeEnd = randomIdx + indexRange < set.size() ? randomIdx + indexRange : set.size()-1; if(minAngle <= point.z && point.z <= maxAngle){
subSample = set.subList(rangeStart, rangeEnd); // get the sub-sample pointsInAngleRange.add(point);
Collections.shuffle(subSample); // shuffle the list }
List<Vector> randomSample = subSample.subList(0, rangeEnd-rangeStart); // get our random sample
if (!randomSample.contains(point)) {
randomSample.add(point);
} }
return randomSample; // shuffle the list to randomize it
Collections.shuffle(pointsInAngleRange);
// if the list is too small, just return the whole list
if(pointsInAngleRange.size() < subSampleSize){
return pointsInAngleRange;
}
// return a subSample of the list
return pointsInAngleRange.subList(0, subSampleSize);
} }
/** /**
* @param originalList the list which the randomSample of points originated from
* @param randomSample a random subsampling of points from the originalList * @param randomSample a random subsampling of points from the originalList
* @param maxRange the maximum distance away from the line of best fit of the subSample of points for a given point's consensus to count. * @param maxRange the maximum distance away from the line of best fit of the subSample of points for a given point's consensus to count.
* @param consensus the number of points that have to give their consensus for the line of best fit to count as a valid feature. * @param consensus the number of points that have to give their consensus for the line of best fit to count as a valid feature.
*/ */
private void extractFeature(ArrayList<Vector> originalList, List<Vector> randomSample, float maxRange, int consensus){ private void extractFeature(List<Vector> randomSample, float maxRange, int consensus){
// get a line of best fit for this list. // get a line of best fit for this list.
Line bestFit = new Line(randomSample); Line bestFit = new Line(randomSample);
// check that there are enough points in the sample that are less than the maxRange away to form a consensus
int count = 0; int count = 0;
ArrayList<Vector> newRandomSample = new ArrayList<>(); ArrayList<Vector> newRandomSample = new ArrayList<>();
for (Vector v : randomSample) { for (Vector v : randomSample) {
@@ -62,44 +68,66 @@ public class SLAM{
lines.add(bestFit); lines.add(bestFit);
// remove the associated readings from the total available readings. // remove the associated readings from the total available readings.
for (Vector v : newRandomSample) { for (Vector v : newRandomSample) {
originalList.remove(v); this.unassociatedPoints.remove(v);
} }
} }
} }
/** private void fitToPreviousReadings(List<Vector> sample, float maxRange){
* @param newPoints a new scan of points to perform feature detection on // keep track of points that were succesffully associated so they can be removed from the sample at the end
* @param raysPerDegree How many degrees apart are each ray that was cast ArrayList<Vector> pointsToRemove = new ArrayList<>();
*/ // try to associate points from the smaple with pre-existing lines
public void RANSAC(ArrayList<Vector> newPoints, float raysPerDegree){ for(Vector v: sample){
float degreeRange = radians(10/2); // range to randomly sample readings within for(Line l : lines){
int indexRange = (int) (degreeRange / raysPerDegree); if(l.getDistance(v) < maxRange){
int numSampleReadings = 10; // number of readings to randomly sample l.refitLine(v);
// constrain numSampleReadings so that it cant be higher than possible pointsToRemove.add(v);
if(numSampleReadings >= 2 * indexRange){ }
numSampleReadings = 2 * indexRange; }
} }
int consensus = 6; // the number of points that need to lie near a line for it to be considered valid.
for(Vector v : pointsToRemove){
sample.remove(v);
}
}
/**
* @param view a laser scan view
*/
public void RANSAC(View view){
unassociatedPoints.addScan(view.getPos(), view.getScan().getPoints());
float degreeRange = radians(5); // range to randomly sample readings within
int numSampleReadings = 15; // number of readings to randomly sample
int consensus = 10; // the number of points that need to lie near a line for it to be considered valid.
float maxRange = 10; // the maximum distance a point can be away from the line for it to count as a consensus float maxRange = 10; // the maximum distance a point can be away from the line for it to count as a consensus
// this for loop determines the maximum number of trials we're willing to do. // this for loop determines the maximum number of trials we're willing to do.
for(int j = 0; j < 20; j++) { for(int j = 0; j < 20; j++) {
// if there aren't enough points left in the set to form a consensus, we're done. // if there aren't enough points left in the set to form a consensus, we're done.
if(newPoints.size() < consensus){ if(this.unassociatedPoints.size() < maxRange){
break; break;
} }
// get a random sub sample of newPoints within the index range of a given size // get a random angle between -PI and PI
List<Vector> randomSample = this.randomSample(newPoints, indexRange, numSampleReadings); float randomAngle = (float) (2*PI*(random()) - 0.5);
// check if the sub sample forms a valid line and remove the randomSample points if it does. // get a random sub sample of newPoints within the index range of a given size
extractFeature(newPoints, randomSample, maxRange, consensus); List<Vector> randomSample = this.randomSampleInAngleRange(this.unassociatedPoints.getPoints(), numSampleReadings, randomAngle-degreeRange, randomAngle+degreeRange);
if(randomSample.size() >= numSampleReadings){
// try to associate points from the sample with previously made lines
fitToPreviousReadings(randomSample, maxRange);
// check if the sub sample forms a valid line and remove the randomSample points if it does.
extractFeature(randomSample, maxRange, consensus);
}
} }
} }
public void drawLines(){ public void drawFeatures(PApplet proc){
for(Line line : lines){ for(Line line : lines){
line.draw(proc); line.draw(proc);
} }

View File

@@ -0,0 +1,57 @@
package ScanGraph;
import Graph.Edge;
import Vector.Line;
import Vector.LineInterface;
import Vector.Vector;
import processing.core.PApplet;
import static java.lang.Math.PI;
public class ScanEdge extends Edge implements LineInterface {
protected ScanPoint vStart;
protected ScanPoint vEnd;
protected Line line;
public ScanEdge(ScanPoint vStart, ScanPoint vEnd){
super(vStart, vEnd);
this.vStart = vStart;
this.vEnd = vEnd;
this.line = new Line(vStart.getPos(), vEnd.getPos());
}
public Vector getDirection(){
return line.getDirection();
}
public Vector getPosition(){
return line.getPosition();
}
public float getLength(){
return line.getLength();
}
public float getAngle(){
return line.getAngle();
}
public Vector endPoint(){
return line.endPoint();
}
public float getDistance(Vector point){
return line.getDistance(point);
}
public void draw(PApplet proc){
line.draw(proc);
Vector leftFlange = line.getDirection().rotate2D((float)(-3*PI/4)).normalize().mul(20);
Vector rightFlange = line.getDirection().rotate2D((float) (3*PI/4)).normalize().mul(20);
Line l1 = new Line(line.endPoint(), line.endPoint().add(leftFlange));
Line l2 = new Line(line.endPoint(), line.endPoint().add(rightFlange));
l1.draw(proc);
l2.draw(proc);
}
}

View File

@@ -0,0 +1,46 @@
package ScanGraph;
import Graph.Graph;
import Graph.Vertex;
import Vector.Vector;
import org.ejml.simple.SimpleMatrix;
import org.ejml.simple.SimpleSVD;
import java.util.ArrayList;
public class ScanGraph extends Graph {
ScanPoint lastPoint;
public ScanGraph(ScanPoint startingPoint) {
super();
this.lastPoint = startingPoint;
}
public void addEdge(ScanPoint vEnd) {
addVertex(vEnd);
ScanEdge edge = new ScanEdge(this.lastPoint, vEnd);
adjList.get((Vertex) this.lastPoint).add(edge);
this.lastPoint = vEnd;
}
/**
* @param newScan the scan to match
* @return null if no match can be found, or an existing scan the matches the new scan.
* @brief Get a new scan in and try to match it with all other scans in the graph
*/
private ScanPoint getAssociatedScan(ScanPoint newScan) {
ScanMatcher matcher = new ScanMatcher();
ScanPoint matchedScan = null;
// go through all of our available scans and try to match the new scan with the old scans. If no match can be found return null
for (Vertex v : adjList.keySet()) {
ScanPoint referenceScan = (ScanPoint) v;
matchedScan = matcher.iterativeScanMatch(referenceScan, newScan, 0.1F, 10);
if(matchedScan != null){
break;
}
}
return matchedScan;
}
}

View File

@@ -0,0 +1,301 @@
package ScanGraph;
import Vector.Vector;
import org.ejml.simple.SimpleMatrix;
import org.ejml.simple.SimpleSVD;
import java.util.ArrayList;
import static java.lang.Math.abs;
/**
* @brief A class that can match two point scans together
*/
public class ScanMatcher{
// A 2x2 matrix describing a rotation to apply to the new scan
public SimpleMatrix rotationMatrix = null;
// A 2x1 matrix describing a translation to apply to the new scan
public SimpleMatrix translationVector = null;
public ScanMatcher(){
}
/**
* @brief iteratively calculate new rotation and transpose matrices to determien if the two scans match
* @param referenceScan the scan to be referenced
* @param newScan the scan that will be rotated and moved until it matches the reference scan
* @param iterations The number of iterations that the scan matcher will attempt
* @param errorThreshold The error threshold that the match will have to meet before considering it a valid match
*/
public ScanPoint iterativeScanMatch(ScanPoint referenceScan, ScanPoint newScan, float errorThreshold, int iterations){
// make a copy of the new scan so we don't modify the original
ScanPoint scanBeingMatched = new ScanPoint(newScan);
// calculate the rotation and translation matrices between the two scans
this.calculateRotationAndTranslationMatrices(referenceScan, scanBeingMatched);
SimpleMatrix cumulativeRotationMatrix = new SimpleMatrix(this.rotationMatrix);
SimpleMatrix cumulativeTranslationVector = new SimpleMatrix(this.translationVector);
// iterate through the scan matching algorithm
for (int i = 0; i < iterations; i++) {
// calculate the rotation and translation matrices between the two scans
this.calculateRotationAndTranslationMatrices(referenceScan, scanBeingMatched);
// apply the rotation and translation matrices to the new scan
scanBeingMatched = this.applyRotationAndTranslationMatrices(scanBeingMatched);
// calculate the error between the new scan and the reference scan
float error = this.getError(referenceScan, scanBeingMatched);
// if the error is less than the error threshold, then we have a valid match
if(error < errorThreshold){
this.rotationMatrix = cumulativeRotationMatrix;
this.translationVector = cumulativeTranslationVector;
return scanBeingMatched;
}
// otherwise, we need to keep iterating
// add the rotation and translation matrices to the cumulative rotation and translation matrices
cumulativeRotationMatrix = cumulativeRotationMatrix.mult(this.rotationMatrix);
cumulativeTranslationVector = cumulativeTranslationVector.plus(this.translationVector);
}
// if we get to this point, then we have not found a valid match
return null;
}
/**
* @brief Compute the cross covariance matrix between the new scan and the reference scan
* @return a 2x2 matrix containing the cross covariance matrix
*/
private SimpleMatrix crossCovarianceMatrix(ScanPoint referenceScan, ScanPoint newScan, CorrespondenceMatrix correspondenceMatrix){
Vector referenceScanAveragePosition = correspondenceMatrix.getAverageOldPosition();
Vector newScanAveragePosition = correspondenceMatrix.getAverageNewPosition();
// compute the cross covariance matrix which is given by the formula:
// covariance = the sum from 1 to N of (p_i) * (q_i)^T
// where p_i is the ith point in the new scan and q_i is the ith point in the reference scan and N is the number of points in the scan
// the cross covariance matrix is a 2x2 matrix
float[][] crossCovarianceMatrix = new float[2][2];
for (int i = 0; i < correspondenceMatrix.getOldPointIndices().size(); i++) {
int oldIndex = correspondenceMatrix.getOldPointIndices().get(i);
int newIndex = correspondenceMatrix.getNewPointIndices().get(i);
Vector oldPoint = referenceScan.getPoints().get(oldIndex);
Vector newPoint = newScan.getPoints().get(newIndex);
if (oldPoint != null && newPoint != null) {
Vector oldPointOffset = oldPoint.sub(referenceScanAveragePosition);
Vector newPointOffset = newPoint.sub(newScanAveragePosition);
crossCovarianceMatrix[0][0] += oldPointOffset.x * newPointOffset.x;
crossCovarianceMatrix[0][1] += oldPointOffset.x * newPointOffset.y;
crossCovarianceMatrix[1][0] += oldPointOffset.y * newPointOffset.x;
crossCovarianceMatrix[1][1] += oldPointOffset.y * newPointOffset.y;
}
}
return new SimpleMatrix(crossCovarianceMatrix);
}
/**
* @brief Compute the rotation and translation matrices between the new scan and the reference scan. Then cache them as private variables.
* The rotation matrix is a 2x2 matrix and the translation vector is a 2x1 matrix
*/
public void calculateRotationAndTranslationMatrices(ScanPoint referenceScan, ScanPoint newScan){
CorrespondenceMatrix correspondenceMatrix = new CorrespondenceMatrix(newScan, referenceScan);
// compute the rotation matrix which is given by the formula:
// R = V * U^T
// where V and U are the singular value decomposition of the cross covariance matrix
// the rotation matrix is a 2x2 matrix
SimpleMatrix crossCovarianceMatrixSimple = crossCovarianceMatrix(referenceScan, newScan, correspondenceMatrix);
SimpleSVD<SimpleMatrix> svd = crossCovarianceMatrixSimple.svd();
this.rotationMatrix = svd.getU().mult(svd.getV().transpose());
// calculate what the angle of the rotation matrix is
float angle = (float) Math.atan2(this.rotationMatrix.get(1, 0), this.rotationMatrix.get(0, 0));
// scale the angle by a small amount to make the rotation matrix more accurate
angle*= 1.75F;
this.rotationMatrix = new SimpleMatrix(new double[][]{{Math.cos(angle), -Math.sin(angle)}, {Math.sin(angle), Math.cos(angle)}});
// percentage of the local position to use in the translation calculation
double weightedAverage = 0.9;
SimpleMatrix localNewScanAveragePosition = new SimpleMatrix(correspondenceMatrix.getAverageNewPosition().toArray());//this.averageScanPosition(newScan);
SimpleMatrix globalNewScanAveragePosition = new SimpleMatrix(this.averageScanPosition(newScan));
SimpleMatrix weightedAverageNewScanPostion = localNewScanAveragePosition.scale(weightedAverage).plus(globalNewScanAveragePosition.scale(1-weightedAverage));
SimpleMatrix localReferenceScanAveragePosition = new SimpleMatrix(correspondenceMatrix.getAverageOldPosition().toArray()); //this.averageScanPosition(referenceScan);
SimpleMatrix globalReferenceScanAveragePosition = new SimpleMatrix(this.averageScanPosition(referenceScan));
SimpleMatrix weightedAverageReferenceScanPostion = localReferenceScanAveragePosition.scale(weightedAverage).plus(globalReferenceScanAveragePosition.scale(1-weightedAverage));
this.translationVector = weightedAverageReferenceScanPostion.minus(rotationMatrix.mult(weightedAverageNewScanPostion));
}
public SimpleMatrix getRotationMatrix(){
return this.rotationMatrix;
}
public SimpleMatrix getTranslationVector(){
return this.translationVector;
}
public ScanPoint applyRotationAndTranslationMatrices(ScanPoint newScan){
// copy the new scan so we don't modify the original
ScanPoint tempScan = new ScanPoint(newScan);
// apply the rotation matrix and translation vector to the new scan
for (int i = 0; i < tempScan.getPoints().size(); i++) {
Vector point = tempScan.getPoints().get(i);
if (point != null) {
SimpleMatrix pointMatrix = new SimpleMatrix(point.toArray());
SimpleMatrix newPointMatrix = rotationMatrix.mult(pointMatrix).plus(translationVector);
tempScan.getPoints().set(i, new Vector((float) newPointMatrix.get(0), (float) newPointMatrix.get(1)));
}
}
return tempScan;
}
/**
* @brief Compute the average position of the scan
* @param scan the scan to compute the average position of
* @return a 2x1 matrix containing the x,y coordinates of the average position of the scan
*/
private SimpleMatrix averageScanPosition(ScanPoint scan){
Vector averagePosition = new Vector(0, 0);
int invalidPoints = 0;
for (Vector point : scan.getPoints()) {
if (point != null) {
averagePosition = averagePosition.add(point);
}
else{
invalidPoints++;
}
}
return new SimpleMatrix(averagePosition.div(scan.getPoints().size() - invalidPoints).toArray());
}
public float getError(ScanPoint referenceScan, ScanPoint newScan){
// calculate the error between the new scan and the reference scan
// q is reference scan and p is new scan
// error is given as abs(Q_mean - R * P_mean)
// where Q_mean is the average position of the reference scan
// P_mean is the average position of the new scan
// R is the rotation matrix
SimpleMatrix newScanAveragePosition = averageScanPosition(newScan);
SimpleMatrix referenceScanAveragePosition = averageScanPosition(referenceScan);
SimpleMatrix error = referenceScanAveragePosition.minus(rotationMatrix.mult(newScanAveragePosition));
return (float) abs(error.elementSum());
}
}
/**
* @brief A class to hold the correspondence matrix between two scans
* The correspondence matrix is a 3xN matrix where N is the number of valid points in the scan.
* This calculates the closest point in the old scan for each point in the new scan and gets rid of redundant closest points.
*/
class CorrespondenceMatrix{
private ArrayList<Integer> oldPointIndices = new ArrayList<>();
private ArrayList<Integer> newPointIndices = new ArrayList<>();
private ArrayList<Float> distances = new ArrayList<>();
private Vector averageOldPosition = new Vector(0, 0);
private Vector averageNewPosition = new Vector(0, 0);
CorrespondenceMatrix(ScanPoint newScan, ScanPoint oldScan){
this.calculateCorrespondenceMatrix(newScan, oldScan);
this.calculateAveragePositions(newScan, oldScan);
}
public ArrayList<Integer> getOldPointIndices(){
return this.oldPointIndices;
}
public ArrayList<Integer> getNewPointIndices(){
return this.newPointIndices;
}
public ArrayList<Float> getDistances(){
return this.distances;
}
public Vector getAverageOldPosition(){
return this.averageOldPosition;
}
public Vector getAverageNewPosition(){
return this.averageNewPosition;
}
private void calculateAveragePositions(ScanPoint newScan, ScanPoint oldScan){
int invalidPoints = 0;
for (int i = 0; i < this.oldPointIndices.size(); i++){
int oldIndex = this.oldPointIndices.get(i);
int newIndex = this.newPointIndices.get(i);
Vector oldPoint = oldScan.getPoints().get(oldIndex);
Vector newPoint = newScan.getPoints().get(newIndex);
if (oldPoint != null && newPoint != null) {
this.averageOldPosition = this.averageOldPosition.add(oldPoint);
this.averageNewPosition = this.averageNewPosition.add(newPoint);
}
else{
invalidPoints++;
}
}
this.averageOldPosition = this.averageOldPosition.div(this.oldPointIndices.size() - invalidPoints);
this.averageNewPosition = this.averageNewPosition.div(this.newPointIndices.size() - invalidPoints);
}
/**
* @brief Calculate the correspondence matrix between two scans
* @param newScan the new scan
* @param referenceScan the reference scan
*/
private void calculateCorrespondenceMatrix(ScanPoint newScan, ScanPoint referenceScan) {
for (int newPointIndex = 0; newPointIndex < newScan.getPoints().size(); newPointIndex++) {
Vector newPoint = newScan.getPoints().get(newPointIndex);
// Skip null points in the new scan
if (newPoint == null) {
continue;
}
float closestDistance = Float.MAX_VALUE;
int closestIndex = -1;
for (int oldPointIndex = 0; oldPointIndex < referenceScan.getPoints().size(); oldPointIndex++) {
Vector oldPoint = referenceScan.getPoints().get(oldPointIndex);
// Skip null points in the old scan
if (oldPoint == null) {
continue;
}
float distance = newPoint.sub(oldPoint).mag();
if (distance < closestDistance) {
closestDistance = distance;
closestIndex = oldPointIndex;
}
}
// if we find a closest point...
if (closestIndex != -1) {
// check if the oldPointIndex is already in the list of oldPointIndices
if(this.oldPointIndices.contains(closestIndex)){
int index = this.oldPointIndices.indexOf(closestIndex);
// if the index is already in our list, then we need to check if the new point is closer than the old point
if(this.distances.get(index) > closestDistance){
// if the new point is closer than the old point, then we need to replace the old point with the new point
this.oldPointIndices.set(index, closestIndex);
this.newPointIndices.set(index, newPointIndex);
this.distances.set(index, closestDistance);
}
}
// if the index is not in our list, then we need to add it
else{
this.oldPointIndices.add(closestIndex);
this.newPointIndices.add(newPointIndex);
this.distances.add(closestDistance);
}
}
}
}
}

View File

@@ -0,0 +1,47 @@
package ScanGraph;
import Graph.Vertex;
import Vector.Vector;
import java.util.ArrayList;
public class ScanPoint extends Vertex{
private Vector position;
private float orientation;
private ArrayList<Vector> scan;
public ScanPoint(Vector scanPosition, float orientation, ArrayList<Vector> scan) {
super();
this.position = scanPosition;
this.orientation = orientation;
this.scan = scan;
}
/**
* @brief Copy constructor
* @param other The scan point to copy
*/
public ScanPoint(ScanPoint other){
super();
this.position = new Vector(other.getPos().x, other.getPos().y);
this.orientation = other.getOrientation();
this.scan = new ArrayList<>(other.getPoints());
}
/**
* @return a two eleement float array containing the x and y coordinates of the vertex respectively.
*/
public Vector getPos(){
return position;
}
public float getOrientation(){
return this.orientation;
}
public ArrayList<Vector> getPoints(){
return this.scan;
}
}

59
src/ShortTermMem.java Normal file
View File

@@ -0,0 +1,59 @@
import java.util.ArrayList;
import Vector.Vector;
public class ShortTermMem {
ArrayList<ArrayList<Vector>> scans = new ArrayList<>();
ArrayList<Vector> scanPositions = new ArrayList<>();
ArrayList<Long> scanTimes = new ArrayList<>();
private int size = 0;
public void addScan(Vector scanPosition, ArrayList<Vector> scan){
size += scan.size();
scans.add(scan);
scanPositions.add(scanPosition);
scanTimes.add(System.currentTimeMillis());
purgeScans();
}
public ArrayList<Vector> getPoints(){
ArrayList<Vector> points = new ArrayList<>();
for(ArrayList<Vector> pointList : this.scans){
points.addAll(pointList);
}
return points;
}
public void remove(Vector point){
for(ArrayList<Vector> pointList : this.scans){
int listSize = pointList.size();
pointList.remove(point);
if(listSize - pointList.size() != 0){
size--;
break;
}
}
}
private void purgeScans(){
long currentTime = System.currentTimeMillis();
int i = scanTimes.size();
// loop through the list backwards and remove all scans that are over second old
// we loop backwards to avoid removal conflicts
while(i > 0){
i--;
long dt = currentTime - scanTimes.get(i);
if(dt < 1000){
continue;
}
size -= scans.get(i).size();
scanTimes.remove(i);
scanPositions.remove(i);
scans.remove(i);
}
}
public int size(){
return this.size;
}
}

View File

@@ -1,74 +0,0 @@
import static java.lang.Math.acos;
import static java.lang.Math.sqrt;
public class Vector {
public float x = 0;
public float y = 0;
public float z = 0;
Vector(){}
Vector(float x, float y){
this.x = x;
this.y = y;
}
Vector(float x, float y, float z){
this.x = x;
this.y = y;
this.z = z;
}
Vector add(Vector other){
return new Vector(this.x + other.x, this.y + other.y, this.z + other.z);
}
Vector add(float x, float y){
return new Vector(this.x + x, this.y + y);
}
Vector add(float x, float y, float z){
return new Vector(this.x + x, this.y + y, this.z + z);
}
Vector sub(Vector other){
return new Vector(this.x - other.x, this.y - other.y, this.z - other.z);
}
Vector sub(float x, float y){
return new Vector(this.x - x, this.y - y);
}
Vector sub(float x, float y, float z){
return new Vector(this.x - x, this.y - y, this.z - z);
}
Vector mul(float scalar){
return new Vector(this.x * scalar, this.y * scalar, this.z * scalar);
}
Vector div(float scalar){
return mul(1/scalar);
}
float mag(){
return (float)sqrt(x*x + y*y + z*z);
}
float dot(Vector other){
return x * other.x + y * other.y + z * other.z;
}
Vector cross(Vector other){
return new Vector(this.y*other.z - this.z*other.y, this.z*other.x - this.x*other.z, this.x*other.y - this.y*other.x);
}
Vector normalize(){
float mag = this.mag();
return new Vector(x / mag, y / mag, z / mag);
}
float angleDiff(Vector other){
float dot = this.dot(other);
return (float)acos(dot / (this.mag() * other.mag()));
}
}

114
src/Vector/Line.java Normal file
View File

@@ -0,0 +1,114 @@
package Vector;
import Vector.Vector;
import processing.core.PApplet;
import java.util.ArrayList;
import java.util.List;
import static processing.core.PApplet.*;
public class Line implements LineInterface{
// vector which represents the direction and length of the line from its starting position
protected Vector direction = new Vector(0,0);
// store the starting position of the line
protected Vector position = new Vector(0,0);
protected ArrayList<Vector> points = new ArrayList<>();
public Line(Vector startPosition, Vector endPosition){
direction = endPosition.sub(startPosition);
position = startPosition;
}
/**
* attempt to find the line of best fit for the given points
* @param points the points to get the line of best for
*/
public Line(List<Vector> points){
bestFit(points);
}
public void refitLine(Vector newPoint){
// add the new point to our list
this.points.add(newPoint);
// rerun the bestFit algorithm with the new point
bestFit(new ArrayList<>());
}
// least squares line of best fit algorithm
public void bestFit(List<Vector> fitPoints){
this.points.addAll(fitPoints);
// get the mean of all the points
Vector mean = new Vector();
for(Vector point : points){
mean = mean.add(point);
}
mean = mean.div(points.size());
// this section calculates the direction vector of the line of best fit
Vector direction = new Vector();
float length = 0;
// get the rise and run of the line of best fit
for(Vector point : points){
direction.y += (point.x - mean.x)*(point.y - mean.y); // rise
direction.x += pow((point.x - mean.x),2);
// get the average distance avery point is away from the mean.
float dist = abs(point.sub(mean).mag());
length += dist;
}
length = 2f*length/points.size();
// if the direction is perfectly vertical create a line to represent that.
if(direction.y == 0){
this.direction = new Vector(0, 1);
}
else{
this.direction = new Vector(1, direction.y/direction.x);
}
// scale the direction vector to be the correct length of the line.
this.direction = this.direction.normalize().mul(length);
this.position = mean.sub(this.direction.div(2));
}
public Vector getSlopeIntForm(){
float slope = direction.y / direction.x;
float intercept = position.y - slope * position.x;
return new Vector(slope, intercept);
}
public Vector getDirection(){
return direction;
}
public Vector getPosition(){
return position;
}
public float getLength(){
return direction.mag();
}
public float getAngle(){return atan2(this.direction.y, this.direction.x);}
public Vector endPoint(){
return this.position.add(this.direction);
}
public float getDistance(Vector point){
float line_dist = direction.mag();
if(line_dist == 0) return this.position.sub(point).mag();
Vector l2 = this.endPoint();
float t = ((point.x - position.x) * (l2.x - position.x) + (point.y - position.y) * (l2.y - position.y) + (point.z - position.z) * (l2.z - position.z)) / line_dist;
t = constrain(t, 0, 1);
Vector closestPoint = new Vector(position.x + t * (l2.x - position.x), position.y + t * (l2.y - position.y), position.z + t * (l2.z - position.z));
return closestPoint.sub(point).mag();
}
public void draw(PApplet proc){
proc.line(position.x, position.y, endPoint().x, endPoint().y);
}
}

View File

@@ -0,0 +1,20 @@
package Vector;
import processing.core.PApplet;
public interface LineInterface {
Vector getDirection();
Vector getPosition();
float getLength();
float getAngle();
Vector endPoint();
float getDistance(Vector point);
void draw(PApplet proc);
}

124
src/Vector/Vector.java Normal file
View File

@@ -0,0 +1,124 @@
package Vector;
import org.ejml.simple.SimpleMatrix;
import processing.core.PApplet;
import static java.lang.Math.*;
import static processing.core.PApplet.cos;
import static processing.core.PApplet.sin;
public class Vector {
public float x = 0;
public float y = 0;
public float z = 0;
Vector(){}
public Vector(float x, float y){
this.x = x;
this.y = y;
}
public Vector(float x, float y, float z){
this.x = x;
this.y = y;
this.z = z;
}
public Vector(SimpleMatrix matrix){
// initialize x,y if matrix is 2x1 and x,y,z if matrix is 3x1
if(matrix.getNumRows() == 2){
this.x = (float)matrix.get(0,0);
this.y = (float)matrix.get(1,0);
}else if(matrix.getNumRows() == 3){
this.x = (float)matrix.get(0,0);
this.y = (float)matrix.get(1,0);
this.z = (float)matrix.get(2,0);
}
}
public Vector add(Vector other){
return new Vector(this.x + other.x, this.y + other.y, this.z + other.z);
}
public Vector add(float x, float y){
return new Vector(this.x + x, this.y + y);
}
public Vector add(float x, float y, float z){
return new Vector(this.x + x, this.y + y, this.z + z);
}
public Vector sub(Vector other){
return new Vector(this.x - other.x, this.y - other.y, this.z - other.z);
}
public Vector sub(float x, float y){
return new Vector(this.x - x, this.y - y);
}
public Vector sub(float x, float y, float z){
return new Vector(this.x - x, this.y - y, this.z - z);
}
public Vector mul(float scalar){
return new Vector(this.x * scalar, this.y * scalar, this.z * scalar);
}
public Vector div(float scalar){
return mul(1/scalar);
}
public float mag(){
return (float)sqrt(x*x + y*y + z*z);
}
public float dot(Vector other){
return x * other.x + y * other.y + z * other.z;
}
public Vector cross(Vector other){
return new Vector(this.y*other.z - this.z*other.y, this.z*other.x - this.x*other.z, this.x*other.y - this.y*other.x);
}
public Vector normalize(){
float mag = this.mag();
return new Vector(x / mag, y / mag, z / mag);
}
/**
* @param other
* @return
*/
public float angleDiff(Vector other){
float dot = this.dot(other); // dot product
float det = this.x*other.y - this.y*other.x; // determinant
float angle = (float) atan2(det, dot); // atan2(y, x) or atan2(sin, cos)
return angle;
}
/**
* @return The angle of the vector in radians
*/
public float angle(){
return (float) atan2(y, x);
}
/**
* @brief Rotate a 2D vector by a given angle
* @param angle The angle to rotate the vector by in radians
* @return The rotated vector
*/
public Vector rotate2D(float angle){
float distance = mag();
float currentAngle = this.angle();
return new Vector(cos(currentAngle + angle), sin(currentAngle + angle)).mul(distance);
}
public void draw(PApplet proc){
proc.circle(this.x, this.y, 8);
}
public float[] toArray() {
return new float[]{x, y};
}
}

View File

@@ -1,168 +1,141 @@
import Graph.PointGraph;
import Vector.Vector;
import processing.core.*; import processing.core.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Objects; import ScanGraph.ScanPoint;
import static processing.core.PApplet.*; public class View {
Vector position;
public class View{
Vector pose;
float angle = 0; float angle = 0;
float FOV; float FOV;
ArrayList<Ray> rays = new ArrayList<>(); ArrayList<Ray> rays = new ArrayList<>();
private static PApplet proc; private static PApplet proc;
//the x,y position of the view, what angle it's looking at and its FOV /**
View(PApplet processing, Vector newPose, int numberOfRays, float FOV){ * @brief Constructor for the View class
* @param processing The PApplet that the view will be drawn on
* @param newPose The position of the view
* @param numberOfRays The number of rays that the view will have
* @param FOV The field of view of the view
*/
View(PApplet processing, Vector newPose, int numberOfRays, float FOV) {
proc = processing; proc = processing;
this.pose = newPose; this.position = newPose;
this.FOV = FOV; this.FOV = FOV;
this.setRayNum(numberOfRays, FOV, this.angle); this.setRayNum(numberOfRays, FOV, this.angle);
} }
//sets the number of rays and their starting values in the ray list //sets the number of rays and their starting values in the ray list
public void setRayNum(int numberOfRays, float FOV, float angleOffset){ public void setRayNum(int numberOfRays, float FOV, float angleOffset) {
float rayStep = FOV/numberOfRays; float rayStep = FOV / numberOfRays;
rays.clear(); rays.clear();
float angle = (float)(0.01-angleOffset); //the 0.01 fixes some bugs float angle = (float) (angleOffset); //the 0.01 fixes some bugs
for(int i = 0; i < numberOfRays; i++){ for (int i = 0; i < numberOfRays; i++) {
Ray ray = new Ray(proc, pose, 100000, angle); Ray ray = new Ray(position, angle);
angle = angle + rayStep; angle = angle + rayStep;
rays.add(ray); rays.add(ray);
} }
} }
//sees if the ray will collide with the walls in the wall list /**
public void look(ArrayList<Wall> walls){ * @brief Calculates the points of intersection of the rays with the map
for (Ray ray : rays){ * @param map The map that the view is looking at
ray.castRay(walls); */
ray.drawRay(); public void calculatePointScan(PointGraph map) {
for (Ray ray : rays) {
ray.castRay(map);
if(ray.hasCollided()){
ray.getPoint().draw(proc);
}
} }
} }
//changes the position of the view /**
public void setPos(Vector newPose){ * @brief Sets the position of the view
pose = newPose; * @param newPosition The new position of the view
for(Ray ray : rays){ray.setPos(pose);} */
public void setPos(Vector newPosition) {
position = newPosition;
for (Ray ray : rays) {
ray.setPos(position);
}
} }
//changes the angle of the view /**
public void setAngle(float angle){ * @brief Sets the angle of the view
this.angle = angle; * @param newAngle The new angle of the view
this.setRayNum(rays.size(), this.FOV, angle); */
public void setAngle(float newAngle) {
this.angle = newAngle;
for(Ray ray : rays){
float angleOffset = ray.getAngle() - this.angle;
ray.setAngle(this.angle+angleOffset);
}
} }
//changes the field of view of the view //changes the field of view of the view
public void setFOV(float FOV){ public void setFOV(float FOV) {
this.FOV = FOV; this.FOV = FOV;
this.setRayNum(this.rays.size(), this.FOV, this.angle); this.setRayNum(this.rays.size(), this.FOV, this.angle);
} }
public Vector getPos(){return pose;} /**
* @return The position of the view
*/
public Vector getPos() {
return position;
}
public float getAngle(){return this.angle;} /**
* @return The angle of the view
*/
public float getAngle() {
return this.angle;
}
public float getFOV(){return this.FOV;} /**
* @return The field of view of the view
*/
public float getFOV() {
return this.FOV;
}
public int getRayNum(){return this.rays.size();} /**
* @return The number of rays that the view has
*/
public int getRayNum() {
return this.rays.size();
}
//gets the point that each ray has collided with /**
public ArrayList<Vector> getPoints(){ * @brief Get the most recent scan from the view
* @return A ScanPoint object containing the position, angle and points of the view
*/
public ScanPoint getScan() {
ArrayList<Vector> points = new ArrayList<>(); ArrayList<Vector> points = new ArrayList<>();
for(Ray ray : rays){ for (Ray ray : rays) {
if(!Objects.equals(ray.getPoint(), new Vector(0, 0) { if(ray.hasCollided()){
})){ Vector point = ray.getPoint();
points.add(ray.getPoint()); // store the angle information for that point in the z coordinate
point.z = ray.getAngle();
points.add(point);
} }
} }
return points; return new ScanPoint(this.position,this.angle, points);
}
/**
* @return A list of the angles where a collision was detected
*/
public ArrayList<Float> getAngles(){
ArrayList<Float> angles = new ArrayList<>();
for (Ray ray : rays) {
if (ray.hasCollided()){
angles.add(ray.getAngle());
}
}
return angles;
} }
} }
class Ray{
Vector pose;
int rayLength;
int defaultRayLength;
float angle; // IN RADIANS
private static PApplet proc;
//takes the starting position of the ray, the length of the ray, and it's casting angle (radians)
Ray(PApplet processing, Vector position, int defaultRayLength, float angle){
proc = processing;
this.pose = position;
this.defaultRayLength = defaultRayLength;
this.rayLength = defaultRayLength;
this.angle = angle;
}
public void drawRay(){
proc.line(pose.x, pose.y, (pose.x + cos(angle)*rayLength), (pose.y + sin(angle)*rayLength));
}
//checks to see at what coordinate the ray will collide with an object and sets the ray length to meet that point.
public void castRay(ArrayList<Wall> objects){
this.rayLength = defaultRayLength;
ArrayList<Integer> distances = new ArrayList<>();
//sees what objects it collides with
for(Wall object : objects){
float theta1 = angle;
float theta2 = radians(object.getAngle());
Vector wallPos = object.getPos();
//finds where along the wall the ray collides
float b = (pose.x*sin(theta1) + wallPos.y*cos(theta1) - pose.y*cos(theta1) - wallPos.x*sin(theta1)) / (cos(theta2)*sin(theta1) - sin(theta2)*cos(theta1));
//if the place along the wall is further away than the wall extends, then it didn't collide
if(b < object.getLength() && b > 0){
//finds the length of the ray needed to collide with the wall
float a = (b*sin(theta2) + wallPos.y-pose.y) / sin(theta1);
//add that length to a list
if(a > 0){
distances.add((int)abs(a));
}
}
}
//finds the shortest distance and sets the length of the ray to that distance
if(distances.size() > 0){
for(Integer distance : distances){
if(distance < rayLength){
rayLength = distance;
}
}
}
else this.rayLength = defaultRayLength;
}
public Vector getPos(){ return pose;}
public int getRayLength(){return this.rayLength;}
public float getAngle(){return this.angle;}
public boolean hasCollided(){
return this.defaultRayLength != this.rayLength;
}
//returns the absolute position of the point
public Vector getPoint(){
if(this.rayLength != this.defaultRayLength){
return new Vector(rayLength * (int)cos(this.angle) + pose.x, rayLength * (int)sin(this.angle) + pose.y);
}
else{
return new Vector(0,0);
}
}
public void setPos(Vector newPose){
pose = newPose;
}
public void setRayLength(int rayLength){this.rayLength = rayLength;}
public void setDefaultRayLength(int defaultRayLength){this.defaultRayLength = defaultRayLength;}
public void setAngle(float angle){this.angle = angle;}
}

View File

@@ -1,41 +0,0 @@
import processing.core.*;
import static processing.core.PApplet.*;
public class Wall{
Vector pos;
float angle;
int wallLength;
private static PApplet proc;
int r;
int g;
int b;
Wall(PApplet processing, Vector pos, float angle, int wallLength){
proc = processing;
this.pos = pos;
this.angle = angle;
this.wallLength = wallLength;
r = (int)proc.random(50, 255);
g = (int)proc.random(50, 255);
b = (int)proc.random(50, 255);
}
void drawWall(){
proc.stroke(r,g,b);
proc.line(pos.x, pos.y, (pos.x + cos(radians(angle))*wallLength), (pos.y + sin(radians(angle))*wallLength));
//ellipse((xPos + cos(radians(angle))*wallLength), (yPos + sin(radians(angle))*wallLength), 20, 20);
}
Vector getPos(){
return pos;
}
float getAngle(){
return angle;
}
int getLength(){
return wallLength;
}
}

View File

@@ -1,8 +1,9 @@
import Vector.Vector;
import Vector.Line;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import processing.core.PApplet;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.Random;
import static java.lang.Math.abs; import static java.lang.Math.abs;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@@ -31,6 +32,34 @@ class LineTest{
// make sure the line is starting in the correct place // make sure the line is starting in the correct place
assertFloatEquals(v1.x, line.getPosition().x); assertFloatEquals(v1.x, line.getPosition().x);
assertFloatEquals(v1.y, line.getPosition().y); assertFloatEquals(v1.y, line.getPosition().y);
// make sure the line ends in the correct place
assertFloatEquals(v2.x, line.endPoint().x);
assertFloatEquals(v2.y, line.endPoint().y);
Random rand = new Random();
// repeat this test with 100 random lines
for(int i = 0; i < 100; i++){
v1 = new Vector(rand.nextInt(1001), rand.nextInt(1001));
v2 = new Vector(rand.nextInt(1001), rand.nextInt(1001));
line = new Line(v1, v2);
// make sure the line is pointing in the right direction
lineDirection = v2.sub(v1);
float angleDiff = lineDirection.angleDiff(line.getDirection());
assertFloatEquals(0, angleDiff, 0.001f);
// make sure the line is the correct length
assertFloatEquals(lineDirection.mag(), line.getLength());
// make sure the line is starting in the correct place
assertFloatEquals(v1.x, line.getPosition().x);
assertFloatEquals(v1.y, line.getPosition().y);
// make sure the line ends in the correct place
assertFloatEquals(v2.x, line.endPoint().x);
assertFloatEquals(v2.y, line.endPoint().y);
}
} }
@Test @Test

View File

@@ -0,0 +1,123 @@
import ScanGraph.ScanMatcher;
import ScanGraph.ScanPoint;
import Vector.Vector;
import processing.core.PApplet;
import java.util.ArrayList;
public class MatcherVisualizer extends PApplet{
public static PApplet processing;
ScanPoint referenceScan;
ScanPoint scanToMatch;
ScanPoint scanBeingMatched;
public static void main(String[] args) {
PApplet.main("MatcherVisualizer");
}
public void settings(){
processing = this;
size(1000, 1000);
float connectingAngle = (float) (3 * Math.PI / 9);
// generate two scans rotated by 45 degrees and append them together
Vector descriptor = new Vector(200, 200);
Vector position = new Vector(500, 500);
ScanPoint scan1 = generateScanPoint(position, descriptor, 12);
ScanPoint scan2 = generateScanPoint(position, descriptor.rotate2D(connectingAngle), 12);
Vector p1 = scan1.getPoints().get(11);
Vector p2 = scan2.getPoints().get(11);
ScanPoint scan3 = generateScanPoint(p1, p2.sub(p1), 12);
this.referenceScan = appendScanPoints(scan1, scan2);
this.referenceScan = appendScanPoints(this.referenceScan, scan3);
// generate two scans offset by some amount and rotated by 55 degrees and append them together
Vector rotated = descriptor.rotate2D((float) Math.PI);
Vector offset = new Vector(100, 150);
ScanPoint scan4 = generateScanPoint(position.add(offset), rotated, 12);
ScanPoint scan5 = generateScanPoint(position.add(offset), rotated.rotate2D(connectingAngle), 12);
p1 = scan4.getPoints().get(11);
p2 = scan5.getPoints().get(11);
ScanPoint scan6 = generateScanPoint(p1, p2.sub(p1), 12);
this.scanToMatch = appendScanPoints(scan4, scan5);
this.scanToMatch = appendScanPoints(this.scanToMatch, scan6);
this.scanBeingMatched = new ScanPoint(this.scanToMatch);
}
public void draw(){
iterativeScanMatch();
// background(0);
}
/**
* @brief Generate a scan point from a scan description
* @param offset The offset of the scan point from the origin
* @param scanDescription A vector which describes the length of the line and direction of the line
* @return A scan point with the given offset and scan description
*/
public static ScanPoint generateScanPoint(Vector offset, Vector scanDescription, int numPoints){
// generate a scan point with the given offset and scan description
ArrayList<Vector> scan = new ArrayList<>();
// divide the scan description by the number of points to allow us to scale it back up in the loop
Vector directionVector = scanDescription.div(numPoints-1);
for (int i = 0; i < numPoints; i++) {
scan.add(offset.add(directionVector.mul(i)));
}
return new ScanPoint(new Vector(0, 0), 0, scan);
}
/**
* @brief Append two scan points together
* @param scan1 The first scan point to append
* @param scan2 The second scan point to append
* @return A scan point that is the combination of the two scan points
*/
public static ScanPoint appendScanPoints(ScanPoint scan1, ScanPoint scan2){
ArrayList<Vector> points = new ArrayList<>();
points.addAll(scan1.getPoints());
points.addAll(scan2.getPoints());
return new ScanPoint(new Vector(0, 0), 0, points);
}
/**
* @brief Draw a scan point to the screen
* @param scan The scan point to draw
* @param color The color to draw the scan point
*/
public void drawScan(ScanPoint scan, int[] color) {
processing.stroke(color[0], color[1], color[2]);
processing.fill(color[0], color[1], color[2]);
ArrayList<Vector> points = scan.getPoints();
for (int i = 0; i < points.size() - 1; i++) {
Vector point = points.get(i);
processing.ellipse(point.x, point.y, 5, 5);
}
}
public void iterativeScanMatch() {
background(0);
int[] red = {255, 0, 0};
int[] green = {0, 255, 0};
int[] blue = {0, 0, 255};
drawScan(this.referenceScan, red);
drawScan(this.scanToMatch, green);
// do a single scan match and calculate the error
ScanMatcher matcher = new ScanMatcher();
matcher.calculateRotationAndTranslationMatrices(this.referenceScan, this.scanBeingMatched);
this.scanBeingMatched = matcher.applyRotationAndTranslationMatrices(this.scanBeingMatched);
float error = matcher.getError(this.referenceScan, this.scanBeingMatched);
drawScan(this.scanBeingMatched, blue);
// do an iterative scan match and calculate the error
// ScanPoint matchedScan = matcher.iterativeScanMatch(scan1, scan2, 0.01f, 10);
// float iterativeScanMatchError = matcher.getError(scan1, matchedScan);
}
}

129
tests/ScanMatcherTest.java Normal file
View File

@@ -0,0 +1,129 @@
import ScanGraph.ScanMatcher;
import ScanGraph.ScanPoint;
import org.junit.Before;
import org.junit.jupiter.api.BeforeEach;
import processing.core.PApplet;
import org.junit.jupiter.api.Test;
import Vector.Vector;
import java.util.ArrayList;
import static org.junit.jupiter.api.Assertions.*;
import static processing.core.PApplet.main;
class ScanMatcherTest{
/**
* @brief Generate a scan point from a scan description
* @param offset The offset of the scan point from the origin
* @param scanDescription A vector which describes the length of the line and direction of the line
* @return A scan point with the given offset and scan description
*/
public ScanPoint generateScanPoint(Vector offset, Vector scanDescription, int numPoints){
// generate a scan point with the given offset and scan description
ArrayList<Vector> scan = new ArrayList<>();
// divide the scan description by the number of points to allow us to scale it back up in the loop
Vector directionVector = scanDescription.div(numPoints-1);
for (int i = 0; i < numPoints; i++) {
scan.add(offset.add(directionVector.mul(i)));
}
return new ScanPoint(new Vector(0, 0), 0, scan);
}
/**
* @brief Append two scan points together
* @param scan1 The first scan point to append
* @param scan2 The second scan point to append
* @return A scan point that is the combination of the two scan points
*/
public ScanPoint appendScanPoints(ScanPoint scan1, ScanPoint scan2){
ArrayList<Vector> points = new ArrayList<>();
points.addAll(scan1.getPoints());
points.addAll(scan2.getPoints());
return new ScanPoint(new Vector(0, 0), 0, points);
}
@Test
public void applyRotationAndTranslationMatrices() {
// generate one scan that is level and another that is rotated 45 degrees.
Vector scanDescription = new Vector(10, 0);
ScanPoint referenceScan = generateScanPoint(new Vector(0, 0), scanDescription, 10);
ScanPoint newScan = generateScanPoint(new Vector(0, 0), scanDescription.rotate2D((float) Math.PI / 4), 10);
// calculate the rotation and translation matrices between the two scans
ScanMatcher matcher = new ScanMatcher();
matcher.calculateRotationAndTranslationMatrices(referenceScan, newScan);
// apply the rotation and translation matrices to the new scan
ScanPoint newScanWithRotationAndTranslation = matcher.applyRotationAndTranslationMatrices(newScan);
// Get the first and last points of the new scan with rotation and translation and calculate the angle between them
ArrayList<Vector> points = newScanWithRotationAndTranslation.getPoints();
Vector firstPoint = points.get(0);
Vector lastPoint = points.get(points.size() - 1);
Vector rotatedDirection = lastPoint.sub(firstPoint);
float angle = scanDescription.angleDiff(rotatedDirection);
// The angle between the first and last points should be zero
assertEquals(0, angle);
}
@Test
public void getError() {
// generate two scans that are the same. The error should be zero.
ScanPoint scan1 = generateScanPoint(new Vector(0, 0), new Vector(10, 10), 12);
ScanPoint scan2 = generateScanPoint(new Vector(0, 0), new Vector(10, 10), 12);
ScanMatcher matcher = new ScanMatcher();
matcher.calculateRotationAndTranslationMatrices(scan1, scan2);
assertEquals(0, matcher.getError(scan1, scan2));
// generate two scans that are the same but one is offset by 10 in the y direction. The error should be 10.
scan1 = generateScanPoint(new Vector(0, 0), new Vector(10, 10), 12);
scan2 = generateScanPoint(new Vector(0, 10), new Vector(10, 10), 12);
matcher.calculateRotationAndTranslationMatrices(scan1, scan2);
assertEquals(10, matcher.getError(scan1, scan2));
// generate two scans that are the same but one is rotated by 45 degrees. The error should be near zero.
scan1 = generateScanPoint(new Vector(0, 0), new Vector(10, 10), 12);
scan2 = generateScanPoint(new Vector(0, 0), new Vector(10, 10).rotate2D((float) Math.PI / 4), 12);
matcher.calculateRotationAndTranslationMatrices(scan1, scan2);
assertEquals(0, matcher.getError(scan1, scan2), 0.1);
}
@Test
public void iterativeScanMatch() {
float bendAngle = (float) (5 * Math.PI / 9);
// generate two scans rotated by 45 degrees and append them together
ScanPoint scan1 = generateScanPoint(new Vector(0, 0), new Vector(10, 10), 12);
ScanPoint scan2 = generateScanPoint(new Vector(0, 0), new Vector(10, 10).rotate2D(bendAngle), 12);
ScanPoint scan3 = appendScanPoints(scan1, scan2);
// generate two scans offset by some amount and rotated by 55 degrees and append them together
Vector rotated = (new Vector(10, 10)).rotate2D((float) Math.PI);
ScanPoint scan4 = generateScanPoint(new Vector(10, 10), rotated, 12);
ScanPoint scan5 = generateScanPoint(new Vector(10, 10), rotated.rotate2D(bendAngle), 12);
ScanPoint scan6 = appendScanPoints(scan4, scan5);
// do a single scan match and calculate the error
ScanMatcher matcher = new ScanMatcher();
matcher.calculateRotationAndTranslationMatrices(scan3, scan6);
ScanPoint oneCalcMatch = matcher.applyRotationAndTranslationMatrices(scan6);
float singleScanMatchError = matcher.getError(scan3, oneCalcMatch);
// do an iterative scan match and calculate the error
ScanPoint matchedScan = matcher.iterativeScanMatch(scan1, scan2, 0.0001f, 10);
// if it's null something has gone wrong with the algorithm because these scans can easily be matched.
assertNotNull(matchedScan);
float iterativeScanMatchError = matcher.getError(scan1, matchedScan);
// the iterative scan match should have a lower error than the single scan match
assertTrue(iterativeScanMatchError < singleScanMatchError);
}
}

View File

@@ -1,5 +1,5 @@
import Vector.Vector;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import processing.core.PApplet;
import static java.lang.Math.*; import static java.lang.Math.*;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@@ -28,8 +28,8 @@ class VectorTest{
assertFloatEquals((float)sqrt(x2*x2 + y2*y2), v2.mag()); assertFloatEquals((float)sqrt(x2*x2 + y2*y2), v2.mag());
// test dot product // test dot product
assertFloatEquals((float)(x1*x2+y1*y2), v1.dot(v2)); assertFloatEquals((x1*x2+y1*y2), v1.dot(v2));
assertFloatEquals((float)(x1*x2+y1*y2), v2.dot(v1)); assertFloatEquals((x1*x2+y1*y2), v2.dot(v1));
// test addition // test addition
Vector vSum = v1.add(v2); Vector vSum = v1.add(v2);