Skip to content

Commit 034b281

Browse files
authored
Add C++ example for running VIO using offline data (#10)
1 parent b3b2622 commit 034b281

File tree

10 files changed

+521
-0
lines changed

10 files changed

+521
-0
lines changed

.gitmodules

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[submodule "cpp/offline/lodepng"]
2+
path = cpp/offline/lodepng
3+
url = https://github.com/lvandeve/lodepng
4+
[submodule "cpp/offline/json"]
5+
path = cpp/offline/json
6+
url = https://github.com/nlohmann/json

cpp/offline/CMakeLists.txt

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
cmake_minimum_required(VERSION 3.3)
2+
3+
project(spectacularAI_offline_example)
4+
5+
add_executable(vio_jsonl
6+
vio_jsonl.cpp
7+
input.cpp
8+
)
9+
10+
find_package(spectacularAI REQUIRED)
11+
add_library(lodepng "lodepng/lodepng.cpp")
12+
add_subdirectory(json)
13+
14+
target_link_libraries(vio_jsonl PRIVATE spectacularAI::spectacularAI lodepng nlohmann_json::nlohmann_json)
15+
target_include_directories(vio_jsonl PRIVATE ".")

cpp/offline/README.md

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# SpectacularAI offline example
2+
3+
This example shows how to run basic stereo VIO using offline data.
4+
5+
* Supported platform: Linux
6+
* Dependencies: CMake, FFmpeg (for video input)
7+
8+
## Setup
9+
10+
* Install the Spectacular AI SDK
11+
* Clone the submodules: `cd cpp/offline/target && git submodule update --init --recursive`.
12+
* Build this example using CMake:
13+
14+
```
15+
mkdir target
16+
cd target
17+
cmake -DspectacularAI_DIR=<path/to/spectacularai-sdk/lib/cmake/spectacularAI> ..
18+
make
19+
```
20+
21+
The `-DspectacularAI_DIR` option is not needed is you have used `sudo make install` for the SDK.
22+
23+
## Usage
24+
25+
In the target folder, run `./vio_jsonl -i path/to/data -o out.jsonl`, where
26+
27+
* `-i` specifies the input folder, see details below. If omitted, mock data will be used.
28+
* `-o` specifies output JSONL file. If omitted, prints instead to stdout.
29+
30+
Input data is read from a given folder with the following hierarchy:
31+
32+
```
33+
├── calibration.json
34+
├── vio_config.yaml
35+
├── data.jsonl
36+
├── data.mp4
37+
└── data2.mp4
38+
```
39+
40+
when using video files. And if instead using PNG images:
41+
42+
```
43+
├── calibration.json
44+
├── vio_config.yaml
45+
├── data.jsonl
46+
├── frames1
47+
│   ├── 00000000.png
48+
│   ├── 00000001.png
49+
│   ├── ...
50+
│   └── 00000600.png
51+
└── frames2
52+
├── 00000000.png
53+
├── 00000001.png
54+
├── ...
55+
└── 00000600.png
56+
```
57+
58+
## Debugging
59+
60+
The option `-r <folder_name>` records the session input data and VIO output to the given folder. If the produced video files do not look correct when viewed in a video player, there may be issue with the image data input into the SDK.
61+
62+
## Visualization
63+
64+
To plot the position track from `-o out.jsonl`, you can use `python3 plot_positions.py out.jsonl`.
65+
66+
## Copyright
67+
68+
For the included libraries, see
69+
* [nlohmann/json](https://github.com/nlohmann/json): `json/LICENSE.MIT`
70+
* [lodepng](https://lodev.org/lodepng/): `lodepng/LICENSE`
71+
72+
For access to the C++ SDK, contact us at <https://www.spectacularai.com/#contact>.
73+
Available for multiple OSes and CPU architectures.

cpp/offline/ffmpeg.hpp

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#ifndef SPECTACULAR_AI_OFFLINE_FFMPEG_HPP
2+
#define SPECTACULAR_AI_OFFLINE_FFMPEG_HPP
3+
4+
#include <cassert>
5+
#include <sstream>
6+
7+
// Run a shell command and return its stdout (not stderr).
8+
std::string exec(const std::string &cmd) {
9+
std::array<char, 128> buffer;
10+
std::string result;
11+
std::shared_ptr<FILE> pipe(popen(cmd.c_str(), "r"), pclose);
12+
assert(pipe);
13+
while (!feof(pipe.get())) {
14+
if (fgets(buffer.data(), 128, pipe.get()) != nullptr)
15+
result += buffer.data();
16+
}
17+
return result;
18+
}
19+
20+
bool ffprobeResolution(const std::string &videoPath, int &width, int &height) {
21+
std::string cmd = "ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 " + videoPath;
22+
std::string resolutionText = exec(cmd + " 2>/dev/null");
23+
if (sscanf(resolutionText.c_str(), "%dx%d", &width, &height) == 2) {
24+
return true;
25+
}
26+
assert(false);
27+
return false;
28+
}
29+
30+
struct VideoInput {
31+
private:
32+
FILE *pipe = nullptr;
33+
34+
public:
35+
int width = 0;
36+
int height = 0;
37+
38+
VideoInput(const std::string &videoPath) {
39+
bool success = ffprobeResolution(videoPath, width, height);
40+
assert(success && width > 0 && height > 0);
41+
std::stringstream ss;
42+
ss << "ffmpeg -i " << videoPath
43+
<< " -f rawvideo -vcodec rawvideo -vsync vfr -pix_fmt gray - 2>/dev/null";
44+
pipe = popen(ss.str().c_str(), "r");
45+
assert(pipe);
46+
}
47+
48+
VideoInput(const VideoInput&) = delete;
49+
50+
~VideoInput() {
51+
assert(pipe);
52+
fflush(pipe);
53+
pclose(pipe);
54+
}
55+
56+
bool read(std::vector<uint8_t> &video, int &width, int &height) {
57+
width = this->width;
58+
height = this->height;
59+
assert(pipe);
60+
int n = width * height;
61+
video.resize(n);
62+
int count = std::fread(video.data(), 1, n, pipe);
63+
return count == n;
64+
}
65+
};
66+
67+
#endif

cpp/offline/input.cpp

+222
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
#include "input.hpp"
2+
3+
#include "ffmpeg.hpp"
4+
5+
#include <fstream>
6+
#include <sstream>
7+
8+
#include <nlohmann/json.hpp>
9+
#include <lodepng/lodepng.h>
10+
11+
namespace {
12+
13+
const std::string SEPARATOR = "/";
14+
15+
using json = nlohmann::json;
16+
17+
// Format strings in printf style.
18+
template<typename ... Args>
19+
std::string stringFormat(const std::string &f, Args ... args) {
20+
int n = std::snprintf(nullptr, 0, f.c_str(), args ...) + 1;
21+
assert(n > 0);
22+
auto nn = static_cast<size_t>(n);
23+
auto buf = std::make_unique<char[]>(nn);
24+
std::snprintf(buf.get(), nn, f.c_str(), args ...);
25+
return std::string(buf.get(), buf.get() + nn - 1);
26+
}
27+
28+
std::vector<std::string> findVideos(const std::string &folderPath, bool &isImageFolder) {
29+
isImageFolder = false;
30+
std::vector<std::string> videos;
31+
for (size_t cameraInd = 0; cameraInd < 2; ++cameraInd) {
32+
std::string videoPathNoSuffix = folderPath + SEPARATOR + "data";
33+
if (cameraInd > 0) videoPathNoSuffix += std::to_string(cameraInd + 1);
34+
for (std::string suffix : { "mov", "avi", "mp4", "mkv" }) {
35+
const std::string videoPath = videoPathNoSuffix + "." + suffix;
36+
std::ifstream testFile(videoPath);
37+
if (testFile.is_open()) videos.push_back(videoPath);
38+
}
39+
40+
const std::string imageFolder = stringFormat("%s/frames%d", folderPath.c_str(), cameraInd + 1);
41+
const std::string firstImage = stringFormat("%s/%08d.png", imageFolder.c_str(), 0);
42+
std::ifstream testFile(firstImage);
43+
if (testFile.is_open()) isImageFolder = true;
44+
}
45+
return videos;
46+
}
47+
48+
// Read PNG image to buffer.
49+
bool readImage(
50+
const std::string &filePath,
51+
std::vector<uint8_t> &data,
52+
std::vector<uint8_t> &tmpBuffer,
53+
int &width,
54+
int &height
55+
) {
56+
std::ifstream file(filePath);
57+
if (!file.is_open()) {
58+
printf("No such file %s\n", filePath.c_str());
59+
return false;
60+
}
61+
tmpBuffer.clear();
62+
unsigned w, h;
63+
unsigned error = lodepng::decode(tmpBuffer, w, h, filePath);
64+
if (error) {
65+
printf("Error %s\n", lodepng_error_text(error));
66+
return false;
67+
}
68+
assert(tmpBuffer.size() == 4 * w * h);
69+
data.resize(w * h);
70+
for (size_t i = 0; i < w * h; ++i) {
71+
// Get green channel from RGBA. Any reasonable gray-scale conversion should work for VIO.
72+
data[i] = tmpBuffer.at(4 * i + 1);
73+
}
74+
width = static_cast<int>(w);
75+
height = static_cast<int>(h);
76+
return true;
77+
}
78+
79+
class InputJsonl : public Input {
80+
public:
81+
InputJsonl(const std::string &inputFolderPath) :
82+
inputFolderPath(inputFolderPath)
83+
{
84+
imu = std::make_shared<spectacularAI::Vector3d>(spectacularAI::Vector3d{ 0.0, 0.0, 0.0 });
85+
jsonlFile.open(inputFolderPath + SEPARATOR + "data.jsonl");
86+
if (!jsonlFile.is_open()) {
87+
printf("No data.jsonl file found. Does `%s` exist?\n", inputFolderPath.c_str());
88+
assert(false);
89+
}
90+
videoPaths = findVideos(inputFolderPath, useImageInput);
91+
assert(!videoPaths.empty() || useImageInput);
92+
for (const std::string &videoPath : videoPaths) {
93+
videoInputs.push_back(std::make_unique<VideoInput>(videoPath));
94+
}
95+
}
96+
97+
std::string getConfig() const final {
98+
std::ifstream configFile(inputFolderPath + SEPARATOR + "vio_config.yaml");
99+
if (!configFile.is_open()) {
100+
printf("No vio_config.yaml provided, using default config.\n");
101+
return "";
102+
}
103+
std::ostringstream oss;
104+
oss << configFile.rdbuf();
105+
return oss.str();
106+
}
107+
108+
std::string getCalibration() const final {
109+
std::ifstream calibrationFile(inputFolderPath + SEPARATOR + "calibration.json");
110+
// Calibration is always required.
111+
assert(calibrationFile.is_open());
112+
std::ostringstream oss;
113+
oss << calibrationFile.rdbuf();
114+
return oss.str();
115+
}
116+
117+
bool next(Data &data) final {
118+
if (!std::getline(jsonlFile, line)) return false;
119+
data.video0 = nullptr;
120+
data.video1 = nullptr;
121+
data.accelerometer = nullptr;
122+
data.gyroscope = nullptr;
123+
124+
json j = json::parse(line, nullptr, false); // stream, callback, allow_exceptions
125+
data.timestamp = j["time"].get<double>();
126+
127+
if (j.find("sensor") != j.end()) {
128+
std::array<double, 3> v = j["sensor"]["values"];
129+
*imu = { .x = v[0], .y = v[1], .z = v[2] };
130+
const std::string sensorType = j["sensor"]["type"];
131+
if (sensorType == "gyroscope") {
132+
data.gyroscope = imu;
133+
}
134+
else if (sensorType == "accelerometer") {
135+
data.accelerometer = imu;
136+
}
137+
}
138+
else if (j.find("frames") != j.end()) {
139+
json jFrames = j["frames"];
140+
size_t cameraCount = jFrames.size();
141+
assert(cameraCount >= 1);
142+
int number = j["number"].get<int>();
143+
for (size_t cameraInd = 0; cameraInd < cameraCount; ++cameraInd) {
144+
std::vector<uint8_t> &video = cameraInd == 0 ? video0 : video1;
145+
if (useImageInput) {
146+
std::string filePath = stringFormat("%s/frames%zu/%08zu.png",
147+
inputFolderPath.c_str(), cameraInd + 1, number);
148+
bool success = readImage(filePath, video, tmpBuffer, data.width, data.height);
149+
assert(success);
150+
}
151+
else {
152+
bool success = videoInputs.at(cameraInd)->read(video, data.width, data.height);
153+
assert(success);
154+
}
155+
uint8_t *&dataVideo = cameraInd == 0 ? data.video0 : data.video1;
156+
dataVideo = video.data();
157+
}
158+
}
159+
return true;
160+
}
161+
162+
private:
163+
std::ifstream jsonlFile;
164+
std::string line;
165+
std::shared_ptr<spectacularAI::Vector3d> imu;
166+
const std::string inputFolderPath;
167+
std::vector<uint8_t> video0, video1, tmpBuffer;
168+
bool useImageInput = false;
169+
std::vector<std::string> videoPaths;
170+
std::vector<std::unique_ptr<VideoInput>> videoInputs;
171+
};
172+
173+
class InputMock : public Input {
174+
public:
175+
int n = 0;
176+
const int width;
177+
const int height;
178+
std::vector<uint8_t> video0, video1;
179+
std::shared_ptr<spectacularAI::Vector3d> imu;
180+
181+
InputMock() : width(640), height(480),
182+
video0(width * height), video1(width * height)
183+
{
184+
imu = std::make_shared<spectacularAI::Vector3d>(spectacularAI::Vector3d{ 0.0, 0.0, 0.0 });
185+
printf("Using mock input, VIO may not output anything.\n");
186+
}
187+
188+
bool next(Data &data) final {
189+
const size_t ITERATIONS = 100;
190+
data.video0 = video0.data();
191+
data.video1 = video1.data();
192+
data.width = width;
193+
data.height = height;
194+
data.accelerometer = imu;
195+
data.gyroscope = imu;
196+
data.timestamp += 0.1;
197+
return n++ < ITERATIONS;
198+
}
199+
200+
std::string getConfig() const final {
201+
return "";
202+
}
203+
204+
std::string getCalibration() const final {
205+
return R"({ "cameras": [
206+
{ "focalLengthX": 1.0, "focalLengthY": 1.0, "model": "pinhole" },
207+
{ "focalLengthX": 1.0, "focalLengthY": 1.0, "model": "pinhole" }
208+
] })";
209+
}
210+
};
211+
212+
} // anonymous namespace
213+
214+
std::unique_ptr<Input> Input::buildJsonl(const std::string &inputFolderPath) {
215+
return std::unique_ptr<Input>(
216+
new InputJsonl(inputFolderPath));
217+
}
218+
219+
std::unique_ptr<Input> Input::buildMock() {
220+
return std::unique_ptr<Input>(
221+
new InputMock());
222+
}

0 commit comments

Comments
 (0)