Skip to content

Commit 80ae49d

Browse files
authored
Add visualizations to cpp recording tools (#19)
* Add visualization to cpp record tools (WIP) * Add --no_record option * Add --preview_fps option * Remove std::filesystem usage * Fix typo * Fix voxel size parsing * Serialization improvements * Cleanup OpenGL resources * Improve PoseTrailRenderer performance * Fix crash on Linux * Restructure cpp visualization code * Rename resolution -> preview_resolution
1 parent 797a182 commit 80ae49d

File tree

18 files changed

+908
-240
lines changed

18 files changed

+908
-240
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "cpp/common/3rdparty/json"]
2+
path = cpp/common/3rdparty/json
3+
url = https://github.com/nlohmann/json

cpp/common/3rdparty/json

Submodule json added at 0457de2
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#pragma once
2+
3+
#include <string>
4+
#include <chrono>
5+
#include <sys/stat.h>
6+
#include <sstream>
7+
#include <iomanip>
8+
9+
#ifdef _MSC_VER
10+
#include <direct.h>
11+
#endif
12+
13+
inline int makeDir(const std::string &dir) {
14+
#ifdef _MSC_VER
15+
return _mkdir(dir.c_str());
16+
#else
17+
mode_t mode = 0755;
18+
return mkdir(dir.c_str(), mode);
19+
#endif
20+
}
21+
22+
inline bool folderExists(const std::string &folder) {
23+
#ifdef _MSC_VER
24+
struct _stat info;
25+
if (_stat(folder.c_str(), &info) != 0) return false;
26+
return (info.st_mode & _S_IFDIR) != 0;
27+
#else
28+
struct stat info;
29+
if (stat(folder.c_str(), &info) != 0) return false;
30+
return (info.st_mode & S_IFDIR) != 0;
31+
#endif
32+
}
33+
34+
inline bool createFolders(const std::string &folder) {
35+
int ret = makeDir(folder);
36+
if (ret == 0) return true;
37+
38+
switch (errno) {
39+
case ENOENT: {
40+
size_t pos = folder.find_last_of('/');
41+
if (pos == std::string::npos)
42+
#ifdef _MSC_VER
43+
pos = folder.find_last_of('\\');
44+
if (pos == std::string::npos)
45+
#endif
46+
return false;
47+
if (!createFolders(folder.substr(0, pos)))
48+
return false;
49+
return 0 == makeDir(folder);
50+
}
51+
case EEXIST:
52+
return folderExists(folder);
53+
54+
default:
55+
return false;
56+
}
57+
}
58+
59+
inline void setAutoSubfolder(std::string &recordingFolder) {
60+
auto now = std::chrono::system_clock::now();
61+
auto timePoint = std::chrono::system_clock::to_time_t(now);
62+
std::tm localTime = *std::localtime(&timePoint);
63+
std::ostringstream oss;
64+
oss << recordingFolder;
65+
oss << std::put_time(&localTime, "/%Y-%m-%d_%H-%M-%S");
66+
recordingFolder = oss.str();
67+
}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
#include "serialization.hpp"
2+
#include "helpers.hpp"
3+
4+
#include <iostream>
5+
#include <nlohmann/json.hpp>
6+
7+
namespace spectacularAI {
8+
namespace visualization {
9+
namespace {
10+
11+
// Deleting directory in C++ without <filesystem> is painful...
12+
// Use python instead...
13+
const char* pythonScript =
14+
R"(
15+
import os
16+
17+
def deleteDirectory(folder_path):
18+
# Check if the folder exists
19+
if os.path.exists(folder_path) and os.path.isdir(folder_path):
20+
# Iterate over files in the folder
21+
for file_name in os.listdir(folder_path):
22+
if file_name.startswith("pointCloud") or file_name.startswith("json"):
23+
file_path = os.path.join(folder_path, file_name)
24+
try:
25+
# Delete the file
26+
os.remove(file_path)
27+
except Exception as e:
28+
print(f"Error deleting file {file_path}: {e}")
29+
30+
# Check if the folder is empty after deleting files
31+
if not os.listdir(folder_path):
32+
try:
33+
# Delete the folder if it is empty
34+
os.rmdir(folder_path)
35+
except Exception as e:
36+
print(f"Error deleting folder {folder_path}: {e}")
37+
else:
38+
print(f"Folder {folder_path} is not empty.")
39+
else:
40+
print(f"Folder {folder_path} does not exist or is not a directory.")
41+
42+
if __name__ == '__main__':
43+
import argparse
44+
p = argparse.ArgumentParser(__doc__)
45+
p.add_argument("directory", help="Directory to delete")
46+
args = p.parse_args()
47+
deleteDirectory(args.directory)
48+
)";
49+
50+
// Runs Python visualization using system()
51+
bool cleanUpTempDirectoryPython(const std::string &directory) {
52+
// Create a temporary Python script
53+
std::ofstream tempFile("spectacularAI_serializer_cleanup.py");
54+
tempFile << pythonScript;
55+
tempFile.close();
56+
57+
// Execute the Python script using system()
58+
std::string pythonCommand = "python spectacularAI_serializer_cleanup.py " + directory;
59+
int result = system(pythonCommand.c_str());
60+
61+
// Delete the temporary Python script
62+
std::remove("spectacularAI_serializer_cleanup.py");
63+
64+
return result == 0;
65+
}
66+
67+
nlohmann::json serializeCamera(const spectacularAI::Camera &camera) {
68+
float near = 0.01f, far = 100.0f;
69+
const Matrix3d &intrinsics = camera.getIntrinsicMatrix();
70+
const Matrix4d &projectionMatrixOpenGL = camera.getProjectionMatrixOpenGL(near, far);
71+
72+
nlohmann::json json;
73+
json["intrinsics"] = intrinsics;
74+
json["projectionMatrixOpenGL"] = projectionMatrixOpenGL;
75+
return json;
76+
}
77+
78+
void serializePointCloud(const std::string &filename, std::shared_ptr<spectacularAI::mapping::PointCloud> pointCloud) {
79+
std::ofstream outputStream(filename.c_str(), std::ios::out | std::ios::binary | std::ios::trunc);
80+
if (!outputStream.is_open()) {
81+
throw std::runtime_error("Failed to create temporary file <" + filename + "> for pointcloud serialization.");
82+
}
83+
84+
std::size_t points = pointCloud->size();
85+
if (points > 0) {
86+
outputStream.write(
87+
reinterpret_cast<const char*>(pointCloud->getPositionData()),
88+
sizeof(spectacularAI::Vector3f) * points);
89+
90+
if (pointCloud->hasNormals()) {
91+
outputStream.write(
92+
reinterpret_cast<const char*>(pointCloud->getNormalData()),
93+
sizeof(spectacularAI::Vector3f) * points);
94+
}
95+
96+
if (pointCloud->hasColors()) {
97+
outputStream.write(
98+
reinterpret_cast<const char*>(pointCloud->getRGB24Data()),
99+
sizeof(std::uint8_t) * 3 * points);
100+
}
101+
}
102+
103+
outputStream.close();
104+
}
105+
106+
} // anonymous namespace
107+
108+
Serializer::Serializer(const std::string &folder) : folder(folder) {
109+
if (!folderExists(folder)) createFolders(folder);
110+
111+
std::string filename = folder + "/json";
112+
outputStream = std::ofstream(filename.c_str(), std::ios::out | std::ios::binary | std::ios::trunc);
113+
if (!outputStream.is_open()) {
114+
throw std::runtime_error("Failed to create temporary file <" + filename + "> for vio output serialization.");
115+
}
116+
}
117+
118+
Serializer::~Serializer() {
119+
outputStream.close();
120+
121+
if (!cleanUpTempDirectoryPython(folder)) {
122+
std::cerr << "Failed to cleanup temporary serialization directory: " << folder << std::endl;
123+
}
124+
}
125+
126+
void Serializer::serializeVioOutput(spectacularAI::VioOutputPtr vioOutput) {
127+
const spectacularAI::Camera &camera = *vioOutput->getCameraPose(0).camera;
128+
const Matrix4d &cameraToWorld = vioOutput->getCameraPose(0).getCameraToWorldMatrix();
129+
130+
// Only properties used in current visualization are serialized, i.e. add more stuff if needed.
131+
nlohmann::json json;
132+
json["cameraPoses"] = {
133+
{
134+
{"camera", serializeCamera(camera)},
135+
{"cameraToWorld", cameraToWorld}
136+
}
137+
};
138+
json["trackingStatus"] = static_cast<int32_t>(vioOutput->status);
139+
140+
std::string jsonStr = json.dump();
141+
uint32_t jsonLength = jsonStr.length();
142+
143+
MessageHeader header = {
144+
.magicBytes = MAGIC_BYTES,
145+
.messageId = messageIdCounter,
146+
.jsonSize = jsonLength,
147+
.binarySize = 0
148+
};
149+
150+
std::lock_guard<std::mutex> lock(outputMutex);
151+
outputStream.write(reinterpret_cast<char*>(&header), sizeof(MessageHeader));
152+
outputStream.write(jsonStr.c_str(), jsonStr.size());
153+
outputStream.flush();
154+
messageIdCounter++;
155+
}
156+
157+
void Serializer::serializeMappingOutput(spectacularAI::mapping::MapperOutputPtr mapperOutput) {
158+
std::map<std::string, nlohmann::json> jsonKeyFrames;
159+
std::size_t binaryLength = 0;
160+
161+
for (auto keyFrameId : mapperOutput->updatedKeyFrames) {
162+
auto search = mapperOutput->map->keyFrames.find(keyFrameId);
163+
if (search == mapperOutput->map->keyFrames.end()) continue; // deleted frame, skip
164+
auto& frameSet = search->second->frameSet;
165+
auto& pointCloud = search->second->pointCloud;
166+
const spectacularAI::Camera &camera = *frameSet->primaryFrame->cameraPose.camera;
167+
const Matrix4d &cameraToWorld = frameSet->primaryFrame->cameraPose.getCameraToWorldMatrix();
168+
nlohmann::json keyFrameJson;
169+
keyFrameJson["id"] = std::to_string(keyFrameId);
170+
keyFrameJson["frameSet"] = {
171+
{"primaryFrame", {
172+
{"cameraPose", {
173+
{"camera", serializeCamera(camera)},
174+
{"cameraToWorld", cameraToWorld}
175+
}}
176+
}}
177+
};
178+
std::size_t points = pointCloud->size();
179+
if (points > 0) {
180+
keyFrameJson["pointCloud"] = {
181+
{"size", points },
182+
{"hasNormals", pointCloud->hasNormals() },
183+
{"hasColors", pointCloud->hasColors() }
184+
};
185+
binaryLength += points * sizeof(spectacularAI::Vector3f);
186+
if (pointCloud->hasNormals()) binaryLength += points * sizeof(spectacularAI::Vector3f);
187+
if (pointCloud->hasColors()) binaryLength += points * sizeof(std::uint8_t) * 3;
188+
189+
if (serializedKeyFrameIds.find(keyFrameId) == serializedKeyFrameIds.end()) {
190+
std::stringstream filename;
191+
filename << folder << "/pointCloud" << keyFrameId;
192+
serializedKeyFrameIds.insert(keyFrameId);
193+
serializePointCloud(filename.str(), pointCloud);
194+
}
195+
}
196+
jsonKeyFrames[keyFrameJson["id"]] = keyFrameJson;
197+
}
198+
199+
nlohmann::json json;
200+
json["updatedKeyFrames"] = mapperOutput->updatedKeyFrames;
201+
json["map"] = {{"keyFrames", jsonKeyFrames}};
202+
json["finalMap"] = mapperOutput->finalMap;
203+
204+
std::string jsonStr = json.dump();
205+
uint32_t jsonLength = jsonStr.length();
206+
MessageHeader header = {
207+
.magicBytes = MAGIC_BYTES,
208+
.messageId = messageIdCounter,
209+
.jsonSize = jsonLength,
210+
.binarySize = (uint32_t)binaryLength
211+
};
212+
213+
std::lock_guard<std::mutex> lock(outputMutex);
214+
outputStream.write(reinterpret_cast<char*>(&header), sizeof(MessageHeader));
215+
outputStream.write(jsonStr.c_str(), jsonStr.size());
216+
outputStream.flush();
217+
messageIdCounter++;
218+
}
219+
220+
} // namespace visualization
221+
} // namespace spectacularAI
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#pragma once
2+
3+
#include <array>
4+
#include <fstream>
5+
#include <mutex>
6+
#include <set>
7+
8+
#include <spectacularAI/output.hpp>
9+
#include <spectacularAI/mapping.hpp>
10+
11+
namespace spectacularAI {
12+
namespace visualization {
13+
14+
// Random number indicating start of a MessageHeader
15+
#define MAGIC_BYTES 2727221974
16+
17+
using Matrix3d = std::array<std::array<double, 3>, 3>;
18+
using Matrix4d = std::array<std::array<double, 4>, 4>;
19+
using SAI_BOOL = uint8_t;
20+
21+
struct MessageHeader {
22+
uint32_t magicBytes;
23+
uint32_t messageId; // Counter for debugging
24+
uint32_t jsonSize;
25+
uint32_t binarySize;
26+
};
27+
28+
class Serializer {
29+
public:
30+
Serializer(const std::string &folder);
31+
~Serializer();
32+
33+
void serializeVioOutput(spectacularAI::VioOutputPtr vioOutput);
34+
void serializeMappingOutput(spectacularAI::mapping::MapperOutputPtr mapperOutput);
35+
36+
private:
37+
const std::string folder;
38+
std::mutex outputMutex;
39+
std::ofstream outputStream;
40+
uint32_t messageIdCounter = 0;
41+
std::set<int> serializedKeyFrameIds;
42+
};
43+
44+
} // namespace visualization
45+
} // namespace spectacularAI

0 commit comments

Comments
 (0)