Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/calibrationtool/graphcompleter/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# at the behest of CMake warnings
project(GraphCompleterTest)
cmake_minimum_required(VERSION 3.20)

# NOTE: to build, run:
# cd <this_directory>
# mkdir build
# cmake -B build
# cmake --build build
# then, run /test/runtests.bat (or a similar batch script that you write yourself for unix platforms and, inmportantly, run from the same directory) to run the tests (and hopefully see no failed ones)

set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})

add_library(graphtestlib
./src/graphcompleter.hpp
./src/graphcompleter.cpp)

target_include_directories(graphtestlib PUBLIC include)

add_executable(csvtester0 ./test/cpp/csvtest0.cpp)
add_executable(csvtester1 ./test/cpp/csvtest1.cpp)
add_executable(csvtester2 ./test/cpp/csvtest2.cpp)
add_executable(csvtester3 ./test/cpp/csvtest3.cpp)
add_executable(csvtester4 ./test/cpp/csvtest4.cpp)
add_executable(csvtester5 ./test/cpp/csvtest5.cpp)
add_executable(csvtester6 ./test/cpp/csvtest6.cpp)

target_link_libraries(csvtester0 PRIVATE graphtestlib)
target_link_libraries(csvtester1 PRIVATE graphtestlib)
target_link_libraries(csvtester2 PRIVATE graphtestlib)
target_link_libraries(csvtester3 PRIVATE graphtestlib)
target_link_libraries(csvtester4 PRIVATE graphtestlib)
target_link_libraries(csvtester5 PRIVATE graphtestlib)
target_link_libraries(csvtester6 PRIVATE graphtestlib)
37 changes: 37 additions & 0 deletions src/calibrationtool/graphcompleter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# graphcompleter
Solving abstract graph problems for JARVIS.

Namely, this solves some problems in building an extrinsics inference graph.

Sometimes, a camera won't have a valid path to the base camera, as in, the number of shared checkerboard frames won't exceed some threshold)

This often will be a physical limitation of a setup rather than a matter of simply needing more frames.

The graphcompleter does something similar to [NCams](https://github.com/CMGreenspon/NCams) and creates daisy-chains up to a user-defined maximum depth `k`.

If it can't find a valid path, it will return the recommended upsampling factors to apply to each camera to achieve a valid graph. For instance, if you only have 20 shared frames between two cameras, if you have a validity threshold of 30 shared frames, the recommended upsampling factor for BOTH cameras will be 1.5.

Valid paths are found greedily by camera index. In other words, if a camera can daisy-chain through camera 0 with edge weight 30, it will prompty accept that solution and move on, even if camera 12 is over there with edge weight 1 million. As JARVIS resamples cameras non-uniformly, raw shared frame counts beyond the "critical mass" are actually a poor measure of edge quality in any case.

# Testing notes
Tested in Windows, unfortunately I didn't have access to a linux environment for testing this (my poor laptop does not have the disk space for a VM). Hopefully the only problem that comes of this is that runtests.bat needs to be rewritten for other operating systems.

To test:

- `cd /graphcompleter/test/py/`
- `python -m generateTestCases`
- `cd /graphcompleter/`
- `mkdir build`
- `cmake -B build`
- `cmake --build build`
- `runtests` (only if Windows...)

# Contents
`/src/` contains the source code. Once everything is tested to satisfaction, its files can be included and integrated into the main `calibrationtool` codebase.

`/test/` contains the test cases, including a Python file located at `/test/py/generateTestCases.py`, which should generate the data required to run the tests when running `python -m generateTestCases`.

Surface-level directory contains the `CMakeLists` for building the tests and the `runtests.bat` for executing them (on Windows...)

# Integration notes
Make sure the test cases work (across platforms!) for this siloed version of graphcompleter before integrating!
8 changes: 8 additions & 0 deletions src/calibrationtool/graphcompleter/runtests.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
build\Debug\csvtester0.exe
build\Debug\csvtester1.exe
build\Debug\csvtester2.exe
build\Debug\csvtester3.exe
build\Debug\csvtester4.exe
build\Debug\csvtester5.exe
build\Debug\csvtester6.exe
PAUSE
225 changes: 225 additions & 0 deletions src/calibrationtool/graphcompleter/src/graphcompleter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/*******************************************************************************
* File: graphcompleter.cpp
* Created: 22. Aug 2022
* Author: James Goodman
* Contact: [email protected]
* Copyright: 2022 James Goodman
* License: LGPL v2.1
******************************************************************************/

#include "graphcompleter.hpp"

GraphCompleter::GraphCompleter(std::vector<int> *edges, int basecam, int threshold, int maxchain) :
edges(edges), b(basecam), t(threshold), k(maxchain), p(int(edges->size()))
{
n = nodesFromPairs();

try {
testValidGraph();
} catch (std::invalid_argument &e) {
std::cout << "error: " << e.what() << std::endl;
exit(1);
}
};

int GraphCompleter::pairsFromNodes(void) {
return n*(n-1)/2;
}

int GraphCompleter::nodesFromPairs(void) {
return (1 + int(std::sqrt(1+8*p)))/2;
}

void GraphCompleter::testValidGraph(void) {
if (pairsFromNodes() != p) {
throw std::invalid_argument("edges vector does not have a valid number of pairs.");
}
}

int GraphCompleter::oddCeilRoot(int x) {
int res = int(std::sqrt(x));

if (int(std::pow(res,2))<x || res%2==0) {
res = 2*((res-1)/2 + 1) + 1;
}

return res;
}

int GraphCompleter::getRow(int idx) {
if (idx >= p || idx < 0) {
throw std::invalid_argument("index argument to getRow exceeded array extents.");
}
return n - (1 + oddCeilRoot(4*int(std::pow(n,2))-4*n+1-8*idx) )/2;
}

int GraphCompleter::getCol(int idx, int row) {
if (idx >= p || row >= n || idx < 0 || row < 0) {
throw std::invalid_argument("index or row argument to getCol exceeded array extents.");
}
return idx + row + 1 - row*n + (row*(row+1))/2;
}


int GraphCompleter::getIndex(int row_, int col_) {
// row should always be the smaller of the two
std::pair<int,int> rowcol = std::minmax(row_,col_);

int &row = rowcol.first;
int &col = rowcol.second;

if (row >= (n-1) || col >= n || row < 0 || col < 0) {
throw std::invalid_argument("row_ or col_ argument to getIndex exceeded array extents.");
}
return row*n-(row*(row+1))/2+col-row-1;
}

std::vector<std::vector<int>> GraphCompleter::getPathsToBase(void) {
return pathsToBase;
}

std::vector<float> GraphCompleter::getResampleFactors(void) {
return resampleFactors;
}

float GraphCompleter::getFrameAdd(int edgeIndex) {
// think of "frame add" as: recommended upsampling factor, minus one (will always be zero if edge is above threshold)
if (edges->at(edgeIndex) == 0) {
return std::numeric_limits<float>::max(); // no dividing by zero!
}
if (edges->at(edgeIndex) < t) {
return float(t - edges->at(edgeIndex)) / float(edges->at(edgeIndex));
} else {
return 0;
}
}

bool GraphCompleter::completeGraph(void) {
// hierarchical depth progression like Ncams does: O(kp)
// Kruskal's algorithm: O(p log n), but ignores the b and k parameters, which the JARVIS framework would like to give users control over
// (b to permit an intuitive coordinate frame, k to mitigate the instability that daisy-chaining can introduce to extrinsics inference)
// in any case, it's probably safe to assume that k will be on the order of log n anyway
// ergo, hierarchical depth progression it is!

std::vector<std::vector<int>> &paths = pathsToBase;
paths.clear(); // idempotence
std::vector<float> pathlens; // in terms of frame adds, not frame counts!

// start with depth=0 case, wherein you actually first populate paths & pathlens
bool valid = true;

for (int i = 0; i < n; ++i) {
std::vector<int> path;
float pathlen;
if (b != i) {
int pairIndex = getIndex(b,i);
path.push_back(pairIndex);
pathlen = getFrameAdd(pairIndex);

if (pathlen > 0) {
valid = false;
}
} else {
pathlen = 0; // the base camera is ground truth!
}

paths.push_back(path);
pathlens.push_back(pathlen);
}

// now iterate for each depth until k
int depth = 1;
while (depth < k && !valid) {
valid = true; // set this back to false if you fail to find a valid path for EVERY node

// save a snapshot of the current state to avoid accidentally testing super long daisy chains as you update state
// there might be a more memory-efficient way to do this
std::vector<float> templens = pathlens;
std::vector<std::vector<int>> temppaths = paths;

for (int i = 0; i < (n-1); ++i) {
for (int j = (i+1); j < n; ++j) {
int pairIndex = getIndex(i,j);
float pathleni = templens[j];
float pathlenj = templens[i];
float frameadd = getFrameAdd(pairIndex);

// conditionality so that path lengths are now the sum of nodal resampling factors, rather than merely being assessed at the edges
if (pathleni == 0) {
pathleni += 2*frameadd; // two cameras must now be resampled
} else if ( getFrameAdd(temppaths[j].back()) < frameadd ) {
// new camera must be resampled, and previous one must be further upsampled
pathleni += 2*frameadd;
pathleni -= getFrameAdd(temppaths[j].back());
} else {
// just the latest camera needs to be upsampled, the previous one has enough new frames to cover it
pathleni += frameadd;
}

if (pathlenj==0) {
pathlenj += 2*frameadd;
} else if (getFrameAdd(temppaths[i].back()) < frameadd ) {
pathlenj += 2*frameadd;
pathlenj -= getFrameAdd(temppaths[i].back());
} else {
pathlenj += frameadd;
}

if (pathleni < pathlens[i]) {
pathlens[i] = pathleni;
paths[i] = temppaths[j];
paths[i].push_back(pairIndex);
}

if (pathlenj < pathlens[j]) {
pathlens[j] = pathlenj;
paths[j] = temppaths[i];
paths[j].push_back(pairIndex);
}
}

// check to see if you're still on pace to produce a valid tree, or if you still haven't found *the one*
if (pathlens[i]>0) {
valid = false;
}
}
// make sure to check the LAST index, too! (since "i" only iterates up to, but not THROUGH, n-1)
if (pathlens[n-1]>0) {
valid = false;
}
++depth;
}

// now determine the upsampling factors you want to use:

// init (for idempotence)
resampleFactors.clear();
for (int i = 0; i < paths.size(); ++i) {
resampleFactors.push_back(1); // resampling factor of 1 = change nothing.
}

// compute
for (int i = 0; i < paths.size(); ++i) {
if (pathlens[i] > 0) {
for (int j=0; j<paths[i].size(); ++j) {
if (edges->at(paths[i][j]) < t) {
float candidate = 1 + getFrameAdd(paths[i][j]);
int row = getRow(paths[i][j]);
int col = getCol(paths[i][j],row);

if (resampleFactors[row] < candidate) {
resampleFactors[row] = candidate;
}

if (resampleFactors[col] < candidate) {
resampleFactors[col] = candidate;
}
}
}
}
}

// and now return if the path was valid or not
return valid;

}
60 changes: 60 additions & 0 deletions src/calibrationtool/graphcompleter/src/graphcompleter.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*******************************************************************************
* File: graphcompleter.hpp
* Created: 22. Aug 2022
* Author: James Goodman
* Contact: [email protected]
* Copyright: 2022 James Goodman
* License: LGPL v2.1
******************************************************************************/

// include guard: classic
#ifndef GRAPHCOMPLETER_H
#define GRAPHCOMPLETER_H

#include <vector> // vector, naturally
#include <cmath> // sqrt and pow
#include <stdexcept> // exceptions
#include <algorithm> // minmax, stable_sort
#include <iostream> // implementation of error messaging
#include <limits> // numeric_limits for handling division-by-zero edge cases

class GraphCompleter {
private:
std::vector<int> *edges; // vector of edge weights. more intuitively represented as a symmetric matrix of shared frame counts between each pair of cameras, this class assumes these data have been converted to the more memory-efficient vector form.

std::vector<std::vector<int>> pathsToBase; // for each node, the best path the base camera can take to reach it.
std::vector<float> resampleFactors; // recommended resampling factors for each of the cameras

int k; // maximum valid length of a daisy chain
int t; // threshold frame count for valid edge
int n; // number of cameras (nodes)
int p; // number of pairs (edges)
int b; // index of baseline camera

int pairsFromNodes(void);

int nodesFromPairs(void);

void testValidGraph(void);

int oddCeilRoot(int x);

int getRow(int idx);

int getCol(int idx, int row);

int getIndex(int row_, int col_);

float getFrameAdd(int edgeIndex);

public:
explicit GraphCompleter(std::vector<int> *edges, int basecam, int threshold, int maxchain);

std::vector<std::vector<int>> getPathsToBase(void);

std::vector<float> getResampleFactors(void);

bool completeGraph(void);
};

#endif
Loading