16 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
36 changed files with 822 additions and 151 deletions

3
.gitignore vendored
View File

@@ -27,3 +27,6 @@ bin/
### 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" />

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.

112
map.txt
View File

@@ -1,112 +0,0 @@
numVerts,60
numEdges,50
vert,206.0,522.0
vert,554.0,422.0
vert,525.0,281.0
vert,404.0,666.0
vert,482.0,615.0
vert,545.0,326.0
vert,520.0,338.0
vert,713.0,810.0
vert,401.0,616.0
vert,520.0,399.0
vert,367.0,375.0
vert,599.0,418.0
vert,591.0,332.0
vert,483.0,810.0
vert,602.0,652.0
vert,711.0,234.0
vert,366.0,275.0
vert,611.0,359.0
vert,570.0,324.0
vert,302.0,249.0
vert,259.0,476.0
vert,411.0,811.0
vert,503.0,375.0
vert,168.0,471.0
vert,603.0,346.0
vert,601.0,491.0
vert,165.0,235.0
vert,405.0,237.0
vert,606.0,811.0
vert,165.0,809.0
vert,213.0,812.0
vert,165.0,521.0
vert,223.0,273.0
vert,294.0,378.0
vert,267.0,616.0
vert,493.0,238.0
vert,403.0,483.0
vert,210.0,380.0
vert,330.0,376.0
vert,714.0,488.0
vert,603.0,537.0
vert,715.0,655.0
vert,224.0,247.0
vert,267.0,723.0
vert,503.0,357.0
vert,618.0,397.0
vert,713.0,539.0
vert,334.0,727.0
vert,485.0,693.0
vert,211.0,346.0
vert,300.0,277.0
vert,294.0,347.0
vert,577.0,423.0
vert,531.0,414.0
vert,329.0,345.0
vert,327.0,615.0
vert,367.0,347.0
vert,304.0,479.0
vert,621.0,379.0
vert,487.0,485.0
edge,start,1,end,53
edge,start,2,end,16
edge,start,3,end,21
edge,start,5,end,18
edge,end,5,start,6
edge,start,7,end,15
edge,start,9,end,22
edge,start,11,end,52
edge,start,12,end,24
edge,start,14,end,41
edge,start,14,end,28
edge,start,15,end,35
edge,start,17,end,58
edge,end,12,start,18
edge,start,19,end,42
edge,start,19,end,50
edge,start,22,end,44
edge,end,20,start,23
edge,end,17,start,24
edge,start,26,end,29
edge,end,26,start,27
edge,end,7,start,29
edge,end,0,start,30
edge,end,0,start,31
edge,start,32,end,42
edge,start,32,end,50
edge,end,8,start,36
edge,start,36,end,57
edge,start,37,end,49
edge,end,33,start,37
edge,start,38,end,54
edge,end,10,start,38
edge,end,25,start,40
edge,start,40,end,46
edge,end,34,start,43
edge,start,43,end,47
edge,end,6,start,44
edge,end,11,start,45
edge,end,13,start,48
edge,end,49,start,51
edge,end,33,start,51
edge,end,1,start,52
edge,end,9,start,53
edge,end,34,start,55
edge,end,47,start,55
edge,end,54,start,56
edge,end,10,start,56
edge,end,45,start,58
edge,end,4,start,59
edge,end,39,start,59

View File

@@ -50,7 +50,7 @@ public class Car{
//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(PointGraph map){ void updateScan(PointGraph map){
for(View view : views){ for(View view : views){
view.look(map); view.calculatePointScan(map);
slam.RANSAC(view); slam.RANSAC(view);
} }

View File

@@ -20,7 +20,7 @@ public class Edge {
* @param vStart the vertex the edge starts at * @param vStart the vertex the edge starts at
* @param vEnd the vertex the edge ends at * @param vEnd the vertex the edge ends at
*/ */
Edge(Vertex vStart, Vertex vEnd){ protected Edge(Vertex vStart, Vertex vEnd){
this.vStart = vStart; this.vStart = vStart;
this.vEnd = vEnd; this.vEnd = vEnd;
} }

View File

@@ -52,10 +52,17 @@ public class PointGraphWriter {
file.close(); file.close();
} }
public PointGraph loadFile(String filename) throws FileNotFoundException, NumberFormatException { public PointGraph loadFile(String filename) throws NumberFormatException {
PointGraph g = new PointGraph(); PointGraph g = new PointGraph();
File file = new File(filename); File file = new File(filename);
Scanner reader = new Scanner(file); Scanner reader;
try {
reader = new Scanner(file);
}
catch (FileNotFoundException e){
System.out.println("File not found");
return g;
}
ArrayList<PointVertex> vertices = new ArrayList<>(); ArrayList<PointVertex> vertices = new ArrayList<>();
while(reader.hasNextLine()){ while(reader.hasNextLine()){
String line = reader.nextLine(); String line = reader.nextLine();

View File

@@ -4,6 +4,7 @@ import processing.core.PApplet;
public class PointVertex extends Vertex { public class PointVertex extends Vertex {
private Vector position; private Vector position;
private int[] color = new int[]{127, 255, 0, 0}; private int[] color = new int[]{127, 255, 0, 0};
/** /**

View File

@@ -23,7 +23,7 @@ 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(360,360); car.addView(90,180);
// for(int i = 0; i < 10; i++){ // for(int i = 0; i < 10; i++){
// PointVertex vStart = new PointVertex(random(50, 950), random(50, 950)); // PointVertex vStart = new PointVertex(random(50, 950), random(50, 950));
@@ -40,7 +40,6 @@ public class Processing extends PApplet {
car.drawCar(map, SLAMIsHidden); car.drawCar(map, SLAMIsHidden);
strokeWeight(2); strokeWeight(2);
stroke(255); stroke(255);
//car.drive(new int[] {0, 0});
} }
public void keyPressed(){ public void keyPressed(){
@@ -80,15 +79,10 @@ public class Processing extends PApplet {
} }
if(key == 'l'){ if(key == 'l'){
System.out.println("Attempting to load a map from file"); System.out.println("Attempting to load a map from file");
try{ PointGraphWriter writer = new PointGraphWriter();
PointGraphWriter writer = new PointGraphWriter(); try {
map = writer.loadFile("map.txt"); map = writer.loadFile("map.txt");
} } catch (NumberFormatException e) {
catch (FileNotFoundException e){
System.out.println("File not found");
e.printStackTrace();
}
catch (NumberFormatException e){
System.out.println("Number format incorrect"); System.out.println("Number format incorrect");
e.printStackTrace(); e.printStackTrace();
} }
@@ -127,7 +121,4 @@ public class Processing extends PApplet {
PointVertex v = new PointVertex(clickPosition); PointVertex v = new PointVertex(clickPosition);
map.addVertex(v); map.addVertex(v);
} }
} }

View File

@@ -1,7 +1,6 @@
import Vector.*; import Vector.*;
import processing.core.*; import processing.core.*;
import java.lang.reflect.Array;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -96,7 +95,7 @@ public class SLAM{
* @param view a laser scan view * @param view a laser scan view
*/ */
public void RANSAC(View view){ public void RANSAC(View view){
unassociatedPoints.addScan(view.getPos(), view.getPoints()); unassociatedPoints.addScan(view.getPos(), view.getScan().getPoints());
float degreeRange = radians(5); // range to randomly sample readings within float degreeRange = radians(5); // range to randomly sample readings within
int numSampleReadings = 15; // number of readings to randomly sample int numSampleReadings = 15; // number of readings to randomly sample

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;
}
}

View File

@@ -1,5 +1,6 @@
package Vector; package Vector;
import org.ejml.simple.SimpleMatrix;
import processing.core.PApplet; import processing.core.PApplet;
import static java.lang.Math.*; import static java.lang.Math.*;
@@ -23,6 +24,18 @@ public class Vector {
this.z = z; 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){ public Vector add(Vector other){
return new Vector(this.x + other.x, this.y + other.y, this.z + other.z); return new Vector(this.x + other.x, this.y + other.y, this.z + other.z);
} }
@@ -83,10 +96,18 @@ public class Vector {
return angle; return angle;
} }
/**
* @return The angle of the vector in radians
*/
public float angle(){ public float angle(){
return (float) atan2(y, x); 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){ public Vector rotate2D(float angle){
float distance = mag(); float distance = mag();
float currentAngle = this.angle(); float currentAngle = this.angle();
@@ -96,4 +117,8 @@ public class Vector {
public void draw(PApplet proc){ public void draw(PApplet proc){
proc.circle(this.x, this.y, 8); proc.circle(this.x, this.y, 8);
} }
public float[] toArray() {
return new float[]{x, y};
}
} }

View File

@@ -2,18 +2,25 @@ import Graph.PointGraph;
import Vector.Vector; import Vector.Vector;
import processing.core.*; import processing.core.*;
import java.util.ArrayList; import java.util.ArrayList;
import ScanGraph.ScanPoint;
public class View { public class View {
Vector pose; Vector position;
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 /**
* @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) { 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);
} }
@@ -24,34 +31,42 @@ public class View {
rays.clear(); rays.clear();
float angle = (float) (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(pose, 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(PointGraph map) { * @brief Calculates the points of intersection of the rays with the map
* @param map The map that the view is looking at
*/
public void calculatePointScan(PointGraph map) {
for (Ray ray : rays) { for (Ray ray : rays) {
ray.castRay(map); ray.castRay(map);
if(ray.hasCollided()){ if(ray.hasCollided()){
ray.getPoint().draw(proc); ray.getPoint().draw(proc);
// ray.drawRay(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
*/
public void setPos(Vector newPosition) {
position = newPosition;
for (Ray ray : rays) { for (Ray ray : rays) {
ray.setPos(pose); 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
*/
public void setAngle(float newAngle) {
this.angle = newAngle;
for(Ray ray : rays){ for(Ray ray : rays){
float angleOffset = ray.getAngle() - this.angle; float angleOffset = ray.getAngle() - this.angle;
ray.setAngle(this.angle+angleOffset); ray.setAngle(this.angle+angleOffset);
@@ -64,24 +79,39 @@ public class View {
this.setRayNum(this.rays.size(), this.FOV, this.angle); this.setRayNum(this.rays.size(), this.FOV, this.angle);
} }
/**
* @return The position of the view
*/
public Vector getPos() { public Vector getPos() {
return pose; return position;
} }
/**
* @return The angle of the view
*/
public float getAngle() { public float getAngle() {
return this.angle; return this.angle;
} }
/**
* @return The field of view of the view
*/
public float getFOV() { public float getFOV() {
return this.FOV; return this.FOV;
} }
/**
* @return The number of rays that the view has
*/
public int getRayNum() { public int getRayNum() {
return this.rays.size(); 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) {
@@ -92,7 +122,7 @@ public class View {
points.add(point); points.add(point);
} }
} }
return points; return new ScanPoint(this.position,this.angle, points);
} }
/** /**

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);
}
}