-
Notifications
You must be signed in to change notification settings - Fork 0
/
vector_field.cc
294 lines (238 loc) · 12.1 KB
/
vector_field.cc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
// OpenCV
#include <opencv2/opencv.hpp>
// BoB robotics 3rd party includes
#include "third_party/path.h"
// BoB robotics includes
#include "navigation/image_database.h"
// PSimpl includes
#include "psimpl.h"
// CLI11 includes
#include "CLI11.hpp"
#include "memory.h"
using namespace BoBRobotics;
using namespace units::literals;
using namespace units::length;
using namespace units::angle;
using namespace units::math;
using namespace units::solid_angle;
//------------------------------------------------------------------------
// Anonymous namespace
//------------------------------------------------------------------------
namespace
{
void processRoute(const Navigation::ImageDatabase &database, double decimate,
cv::Mat &renderMatFull, cv::Mat &renderMatDecimated,
std::vector<cv::Point2f> &decimatedPoints)
{
std::vector<float> routePointComponents;
routePointComponents.reserve(database.size() * 2);
{
// Reserve temporary vector to hold route points, snapped to integer pixels
std::vector<cv::Point2i> routePointPixels;
routePointPixels.reserve(database.size());
// Loop through route
for(const auto &r : database) {
// Get position of point in cm
const centimeter_t x = r.position[0];
const centimeter_t y = r.position[1];
// Add x and y components of position to vector
routePointComponents.emplace_back(x.value());
routePointComponents.emplace_back(y.value());
// Add x and y pixel values
routePointPixels.emplace_back((int)std::round(x.value()), (int)std::round(y.value()));
}
// Build render matrix from route point pixels
renderMatFull = cv::Mat(routePointPixels, true);
}
// Decimate route points
std::vector<float> decimatedRoutePointComponents;
psimpl::simplify_douglas_peucker<2>(routePointComponents.cbegin(), routePointComponents.cend(),
decimate,
std::back_inserter(decimatedRoutePointComponents));
decimatedPoints.reserve(decimatedRoutePointComponents.size() / 2);
{
// Reserve temporary vector to hold decimated route points, snapped to integer pixels
std::vector<cv::Point2i> decimatedPixels;
decimatedPixels.reserve(decimatedRoutePointComponents.size() / 2);
for(size_t i = 0; i < decimatedRoutePointComponents.size(); i += 2) {
const float x = decimatedRoutePointComponents[i];
const float y = decimatedRoutePointComponents[i + 1];
decimatedPixels.emplace_back((int)std::round(x), (int)std::round(y));
decimatedPoints.emplace_back(x, y);
}
// Build render matrix from decimated pixels
renderMatDecimated = cv::Mat(decimatedPixels, true);
}
}
//------------------------------------------------------------------------
// Get distance to route from point
std::tuple<centimeter_t, cv::Point2f, size_t, degree_t> getNearestPointOnRoute(const cv::Point2f &point,
const std::vector<cv::Point2f> &routePoints)
{
// Loop through points
float shortestDistanceSquared = std::numeric_limits<float>::max();
cv::Point2f nearestPoint;
size_t nearestSegment;
for(size_t i = 0; i < (routePoints.size() - 1); i++) {
// Get vector pointing along segment and it's squared
const cv::Point2f segmentVector = routePoints[i + 1] - routePoints[i];
const float segmentLengthSquared = segmentVector.dot(segmentVector);
// Get vector from start of segment to point
const cv::Point2f segmentStartToPoint = point - routePoints[i];
// Take dot product of two vectors and normalise, clamping at 0 and 1
const float t = std::max(0.0f, std::min(1.0f, segmentStartToPoint.dot(segmentVector) / segmentLengthSquared));
// Find nearest point on the segment
const cv::Point2f nearestPointOnSegment = routePoints[i] + (t * segmentVector);
// Get the vector from here to our point and hence the squared distance
const cv::Point2f shortestSegmentToPoint = point - nearestPointOnSegment;
const float distanceSquared = shortestSegmentToPoint.dot(shortestSegmentToPoint);
// If this is shorter than current best, update current
if(distanceSquared < shortestDistanceSquared) {
shortestDistanceSquared = distanceSquared;
nearestPoint = nearestPointOnSegment;
nearestSegment = i;
}
}
// Get vector in direction of nearest segment and hence heading
const cv::Point2f nearestSegmentVector = routePoints[nearestSegment + 1] - routePoints[nearestSegment];
const degree_t nearestSegmentHeading = radian_t(std::atan2(nearestSegmentVector.y, nearestSegmentVector.x));
// Return shortest distance and position of nearest point
return std::make_tuple(centimeter_t(std::sqrt(shortestDistanceSquared)), nearestPoint, nearestSegment, nearestSegmentHeading);
}
} // Anonymous namespace
int main(int argc, char **argv)
{
// Default command line arguments
cv::Size imSize(120, 25);
std::string routeName = "route5";
std::string variantName = "skymask";
std::string imageGridName = "mid_day";
std::string outputImageName = "grid_image.png";
std::string outputCSVName = "";
std::string memoryType = "PerfectMemory";
bool renderGoodMatches = true;
bool renderBadMatches = false;
bool renderRoute = true;
bool renderDecimatedRoute = true;
double fovDegrees = 90.0;
double decimateDistance = 15.0;
// Configure command line parser
CLI::App app{"BoB robotics 'vector field' renderer"};
app.add_option("--route", routeName, "Name of route", true);
app.add_option("--grid", imageGridName, "Name of image grid", true);
app.add_option("--variant", variantName, "Variant of route and grid to use", true);
app.add_option("--width", imSize.width, "Width of unwrapped image", true);
app.add_option("--height", imSize.height, "Height of unwrapped image", true);
app.add_option("--output-image", outputImageName, "Name of output image to generate", true);
app.add_option("--output-csv", outputCSVName, "Name of output CSV to generate", true);
app.add_option("--decimate-distance", decimateDistance, "Threshold (in cm) for decimating route points", true);
app.add_option("--fov", fovDegrees,
"For 'constrained' memories, what angle (in degrees) on either side of route should snapshots be matched in", true);
app.add_set("--memory-type", memoryType, {"PerfectMemory", "PerfectMemoryConstrained", "InfoMax", "InfoMaxConstrained"},
"Type of memory to use for navigation", true);
/*app.add_flag("--render-good-matches,--no-render-good-matches{false}", renderGoodMatches,
"Should lines be rendered between grid points and 'good' matches");
app.add_flag("--render-bad-matches,!--no-render-bad-matches", renderBadMatches,
"Should lines be rendered between grid points and 'bad' matches");
app.add_flag("--render-route,!--no-render-route", renderRoute,
"Should unprocessed route be rendered");
app.add_flag("--render-decimated-route,!--no-render-decimated-route", renderDecimatedRoute,
"Should decimated route be rendered");*/
// Parse command line arguments
CLI11_PARSE(app, argc, argv);
// Create database from route
const filesystem::path routePath = filesystem::path("routes") / routeName / variantName;
std::cout << routePath << std::endl;
Navigation::ImageDatabase route(routePath);
std::unique_ptr<MemoryBase> memory;
if(memoryType == "PerfectMemory") {
memory.reset(new PerfectMemory(imSize, route,
renderGoodMatches, renderBadMatches));
}
else if(memoryType == "PerfectMemoryConstrained") {
memory.reset(new PerfectMemoryConstrained(imSize, route, degree_t(fovDegrees),
renderGoodMatches, renderBadMatches));
}
else if(memoryType == "InfoMax") {
memory.reset(new InfoMax(imSize, route));
}
else if(memoryType == "InfoMaxConstrained") {
memory.reset(new InfoMaxConstrained(imSize, route, degree_t(fovDegrees)));
}
else {
throw std::runtime_error("Memory type '" + memoryType + "' not supported");
}
// Process routes to get render images
std::vector<cv::Point2f> decimatedRoutePoints;
cv::Mat routePointsMat;
cv::Mat decimatedRoutePointMat;
processRoute(route, decimateDistance, routePointsMat, decimatedRoutePointMat, decimatedRoutePoints);
// Load grid
Navigation::ImageDatabase grid = filesystem::path("image_grids") / imageGridName / variantName;
assert(grid.isGrid());
assert(grid.hasMetadata());
// Read grid dimensions from meta data
std::vector<double> size, seperationMM;
grid.getMetadata()["grid"]["separationMM"] >> seperationMM;
grid.getMetadata()["grid"]["size"] >> size;
assert(size.size() == 3);
assert(seperationMM.size() == 3);
std::cout << size[0] << "x" << size[1] << " grid with " << seperationMM[0] << "x" << seperationMM[1] << "mm squares" << std::endl;
// If a filename is specified, open CSV file other write to std::cout
std::ofstream outputCSVFile;
if(!outputCSVName.empty()) {
outputCSVFile.open(outputCSVName);
}
std::ostream &outputCSV = outputCSVName.empty() ? std::cout : outputCSVFile;
// Write header to CSV file
memory->writeCSVHeader(outputCSV);
outputCSV << std::endl;
// Make a grid image with one pixel per cm
cv::Mat gridImage((int)std::round(size[1] * seperationMM[1] * 0.1),
(int)std::round(size[0] * seperationMM[0] * 0.1),
CV_8UC3, cv::Scalar::all(0));
// Draw route onto image
if(renderRoute) {
cv::polylines(gridImage, routePointsMat, false, CV_RGB(64, 64, 64));
}
if(renderDecimatedRoute) {
cv::polylines(gridImage, decimatedRoutePointMat, false, CV_RGB(255, 255, 255));
}
// Loop through grid entries
size_t numGridPointsWithinROI = 0;
degree_squared_t sumSquareError = 0_sq_deg;
for(const auto &g : grid) {
const centimeter_t x = g.position[0];
const centimeter_t y = g.position[1];
// Get distance from grid point to route
const auto nearestPoint = getNearestPointOnRoute(cv::Point2f(x.value(), y.value()), decimatedRoutePoints);
// If snapshot is within R.O.I.
if(std::get<0>(nearestPoint) < 4_m) {
// Increment count
numGridPointsWithinROI++;
// Load snapshot and resize
cv::Mat snapshot = g.load();
cv::resize(snapshot, snapshot, imSize);
// Test snapshot using memory
memory->test(snapshot, g.heading, std::get<3>(nearestPoint));
// Get magnitude of shortest angle between route and headig
const degree_t angularError = shortestAngleBetween(memory->getBestHeading(), std::get<3>(nearestPoint));
// Add to sum square error
sumSquareError += (angularError * angularError);
// Draw arrow showing vector field
const centimeter_t xEnd = x + (60_cm * memory->getVectorLength() * cos(memory->getBestHeading()));
const centimeter_t yEnd = y + (60_cm * memory->getVectorLength() * sin(memory->getBestHeading()));
cv::arrowedLine(gridImage, cv::Point(x.value(), y.value()), cv::Point(xEnd.value(), yEnd.value()),
CV_RGB(0, 0, 255));
// Write CSV line
memory->writeCSVLine(outputCSV, x, y, angularError);
outputCSV << std::endl;
// Perform any memory-specific additional rendering
memory->render(gridImage, x, y);
// Update output image
cv::imwrite(outputImageName, gridImage);
}
}
std::cout << "RMSE:" << degree_t(sqrt(sumSquareError / (double)numGridPointsWithinROI)) << std::endl;
return EXIT_SUCCESS;
}