Skip to content

Commit cb18316

Browse files
committed
fixed the cmake
2 parents 03790f2 + 1b7dd07 commit cb18316

10 files changed

+521
-70
lines changed

CMakeLists.txt

+24-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ set(OPENSPLAT_BUILD_SIMPLE_TRAINER OFF CACHE BOOL "Build simple trainer applicat
55
set(GPU_RUNTIME "CUDA" CACHE STRING "HIP or CUDA or MPS")
66
set(OPENCV_DIR "OPENCV_DIR-NOTFOUND" CACHE PATH "Path to the OPENCV installation directory")
77
set(OPENSPLAT_MAX_CUDA_COMPATIBILITY OFF CACHE BOOL "Build for maximum CUDA device compatibility")
8+
set(OPENSPLAT_BUILD_VISUALIZER OFF CACHE BOOL "Build visualizer application")
89
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
910

1011
# Read version
@@ -163,6 +164,9 @@ endif()
163164

164165
find_package(Torch REQUIRED)
165166
find_package(OpenCV HINTS "${OPENCV_DIR}" REQUIRED)
167+
if (OPENSPLAT_BUILD_VISUALIZER)
168+
find_package(Pangolin REQUIRED)
169+
endif()
166170

167171
if (NOT WIN32 AND NOT APPLE)
168172
set(CMAKE_CUDA_COMPILER "${CUDA_TOOLKIT_ROOT_DIR}/bin/nvcc")
@@ -216,14 +220,33 @@ endif()
216220
add_library(gsplat_cpu rasterizer/gsplat-cpu/gsplat_cpu.cpp)
217221
target_include_directories(gsplat_cpu PRIVATE ${TORCH_INCLUDE_DIRS})
218222

219-
add_executable(opensplat opensplat.cpp point_io.cpp nerfstudio.cpp model.cpp kdtree_tensor.cpp spherical_harmonics.cpp cv_utils.cpp utils.cpp project_gaussians.cpp rasterize_gaussians.cpp ssim.cpp optim_scheduler.cpp colmap.cpp opensfm.cpp openmvg.cpp input_data.cpp tensor_math.cpp)
223+
set(OPENSPLAT_SRC_FILES opensplat.cpp point_io.cpp nerfstudio.cpp model.cpp
224+
kdtree_tensor.cpp spherical_harmonics.cpp cv_utils.cpp utils.cpp project_gaussians.cpp
225+
rasterize_gaussians.cpp ssim.cpp optim_scheduler.cpp colmap.cpp opensfm.cpp openmvg.cpp input_data.cpp
226+
tensor_math.cpp)
227+
228+
if (OPENSPLAT_BUILD_VISUALIZER)
229+
if (Pangolin_FOUND)
230+
message(STATUS "Found Pangolin. Building visualizer (beta)")
231+
list(APPEND OPENSPLAT_SRC_FILES visualizer.cpp)
232+
add_definitions(-DUSE_VISUALIZATION)
233+
else()
234+
message(FATAL "Pangolin not found. Cannot build visualizer (beta)")
235+
endif()
236+
endif()
237+
238+
add_executable(opensplat ${OPENSPLAT_SRC_FILES})
239+
220240
install(TARGETS opensplat DESTINATION bin)
221241
set_property(TARGET opensplat PROPERTY CXX_STANDARD 17)
222242
target_include_directories(opensplat PRIVATE
223243
${PROJECT_SOURCE_DIR}/rasterizer
224244
${GPU_INCLUDE_DIRS}
225245
)
226246
target_link_libraries(opensplat PUBLIC ${STDPPFS_LIBRARY} ${GPU_LIBRARIES} ${GSPLAT_LIBS} ${TORCH_LIBRARIES} ${OpenCV_LIBS})
247+
if (Pangolin_FOUND)
248+
target_link_libraries(opensplat PUBLIC ${Pangolin_LIBRARIES})
249+
endif()
227250
target_link_libraries(opensplat PRIVATE
228251
nlohmann_json::nlohmann_json
229252
cxxopts::cxxopts

README.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ A free and open source implementation of 3D [gaussian splatting](https://www.you
55
<img src="https://github.com/pierotofy/OpenSplat/assets/1951843/c9327c7c-31ad-402d-a5a5-04f7602ca5f5" width="49%" />
66
<img src="https://github.com/pierotofy/OpenSplat/assets/1951843/eba4ae75-2c88-4c9e-a66b-608b574d085f" width="49%" />
77

8-
OpenSplat takes camera poses + sparse points in [COLMAP](https://colmap.github.io/), [OpenSfM](https://github.com/mapillary/OpenSfM), [ODM](https://github.com/OpenDroneMap/ODM) or [nerfstudio](https://docs.nerf.studio/quickstart/custom_dataset.html) project format and computes a [scene file](https://drive.google.com/file/d/12lmvVWpFlFPL6nxl2e2d-4u4a31RCSKT/view?usp=sharing) (.ply or .splat) that can be later imported for [viewing](https://antimatter15.com/splat/?url=https://splat.uav4geo.com/banana.splat), editing and rendering in other [software](https://github.com/MrNeRF/awesome-3D-gaussian-splatting?tab=readme-ov-file#open-source-implementations).
8+
OpenSplat takes camera poses + sparse points in [COLMAP](https://colmap.github.io/), [OpenSfM](https://github.com/mapillary/OpenSfM), [ODM](https://github.com/OpenDroneMap/ODM) [OpenMVG](https://github.com/OpenMVG/OpenMVG) or [nerfstudio](https://docs.nerf.studio/quickstart/custom_dataset.html) project format and computes a [scene file](https://drive.google.com/file/d/12lmvVWpFlFPL6nxl2e2d-4u4a31RCSKT/view?usp=sharing) (.ply or .splat) that can be later imported for [viewing](https://antimatter15.com/splat/?url=https://splat.uav4geo.com/banana.splat), editing and rendering in other [software](https://github.com/MrNeRF/awesome-3D-gaussian-splatting?tab=readme-ov-file#open-source-implementations).
99

1010
Graphics card recommended, but not required! OpenSplat runs the fastest on NVIDIA, AMD and Apple (Metal) GPUs, but can also run entirely on the CPU (~100x slower).
1111

@@ -140,6 +140,8 @@ If building CPU-only, remove `-DGPU_RUNTIME=MPS`.
140140

141141
:warning: You will probably get a *libc10.dylib can’t be opened because Apple cannot check it for malicious software* error on first run. Open **System Settings** and go to **Privacy & Security** and find the **Allow** button. You might need to repeat this several times until all torch libraries are loaded.
142142

143+
:warning: If you get a *Library not loaded: @rpath/libomp.dylib* error, try running `brew link libomp --force` before running OpenSplat.
144+
143145
## Docker Build
144146

145147
### CUDA
@@ -231,6 +233,14 @@ To generate compressed splats (.splat files), use the `-o` option:
231233
./opensplat /path/to/banana -o banana.splat
232234
```
233235

236+
### Resume
237+
238+
You can resume training of a .PLY file by using the `--resume` option:
239+
240+
```bash
241+
./opensplat /path/to/banana --resume ./splat.ply
242+
```
243+
234244
### AMD GPU Notes
235245

236246
To train a model with AMD GPU using docker container, you can use the following command as a reference:

input_data.cpp

+10-7
Original file line numberDiff line numberDiff line change
@@ -44,22 +44,25 @@ void Camera::loadImage(float downscaleFactor){
4444
if (image.numel()) std::runtime_error("loadImage already called");
4545
std::cout << "Loading " << filePath << std::endl;
4646

47-
float scaleFactor = 1.0f / downscaleFactor;
4847
cv::Mat cImg = imreadRGB(filePath);
4948

5049
float rescaleF = 1.0f;
5150
// If camera intrinsics don't match the image dimensions
5251
if (cImg.rows != height || cImg.cols != width){
5352
rescaleF = static_cast<float>(cImg.rows) / static_cast<float>(height);
5453
}
55-
fx *= scaleFactor * rescaleF;
56-
fy *= scaleFactor * rescaleF;
57-
cx *= scaleFactor * rescaleF;
58-
cy *= scaleFactor * rescaleF;
54+
fx *= rescaleF;
55+
fy *= rescaleF;
56+
cx *= rescaleF;
57+
cy *= rescaleF;
5958

6059
if (downscaleFactor > 1.0f){
61-
float f = 1.0f / downscaleFactor;
62-
cv::resize(cImg, cImg, cv::Size(), f, f, cv::INTER_AREA);
60+
float scaleFactor = 1.0f / downscaleFactor;
61+
cv::resize(cImg, cImg, cv::Size(), scaleFactor, scaleFactor, cv::INTER_AREA);
62+
fx *= scaleFactor;
63+
fy *= scaleFactor;
64+
cx *= scaleFactor;
65+
cy *= scaleFactor;
6366
}
6467

6568
K = getIntrinsicsMatrix();

model.cpp

+186-9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "rasterize_gaussians.hpp"
77
#include "tensor_math.hpp"
88
#include "gsplat.hpp"
9+
#include "utils.hpp"
910

1011
#ifdef USE_HIP
1112
#include <c10/hip/HIPCachingAllocator.h>
@@ -50,6 +51,30 @@ torch::Tensor l1(const torch::Tensor& rendered, const torch::Tensor& gt){
5051
return torch::abs(gt - rendered).mean();
5152
}
5253

54+
void Model::setupOptimizers(){
55+
releaseOptimizers();
56+
57+
meansOpt = new torch::optim::Adam({means}, torch::optim::AdamOptions(0.00016));
58+
scalesOpt = new torch::optim::Adam({scales}, torch::optim::AdamOptions(0.005));
59+
quatsOpt = new torch::optim::Adam({quats}, torch::optim::AdamOptions(0.001));
60+
featuresDcOpt = new torch::optim::Adam({featuresDc}, torch::optim::AdamOptions(0.0025));
61+
featuresRestOpt = new torch::optim::Adam({featuresRest}, torch::optim::AdamOptions(0.000125));
62+
opacitiesOpt = new torch::optim::Adam({opacities}, torch::optim::AdamOptions(0.05));
63+
64+
meansOptScheduler = new OptimScheduler(meansOpt, 0.0000016f, maxSteps);
65+
}
66+
67+
void Model::releaseOptimizers(){
68+
RELEASE_SAFELY(meansOpt);
69+
RELEASE_SAFELY(scalesOpt);
70+
RELEASE_SAFELY(quatsOpt);
71+
RELEASE_SAFELY(featuresDcOpt);
72+
RELEASE_SAFELY(featuresRestOpt);
73+
RELEASE_SAFELY(opacitiesOpt);
74+
75+
RELEASE_SAFELY(meansOptScheduler);
76+
}
77+
5378

5479
torch::Tensor Model::forward(Camera& cam, int step){
5580

@@ -139,13 +164,11 @@ torch::Tensor Model::forward(Camera& cam, int step){
139164
#endif
140165
}
141166

167+
xys.retain_grad();
142168

143169
if (radii.sum().item<float>() == 0.0f)
144170
return backgroundColor.repeat({height, width, 1});
145171

146-
// TODO: is this needed?
147-
xys.retain_grad();
148-
149172
torch::Tensor viewDirs = means.detach() - T.transpose(0, 1).to(device);
150173
viewDirs = viewDirs / viewDirs.norm(2, {-1}, true);
151174
int degreesToUse = (std::min<int>)(step / shDegreeInterval, shDegree);
@@ -281,6 +304,9 @@ void Model::removeFromOptimizer(torch::optim::Adam *optimizer, const torch::Tens
281304
void Model::afterTrain(int step){
282305
torch::NoGradGuard noGrad;
283306

307+
// When radii.sum() == 0
308+
if (!xys.grad().defined()) return;
309+
284310
if (step < stopSplitAt){
285311
torch::Tensor visibleMask = (radii > 0).flatten();
286312

@@ -460,22 +486,22 @@ void Model::afterTrain(int step){
460486
}
461487
}
462488

463-
void Model::save(const std::string &filename){
489+
void Model::save(const std::string &filename, int step){
464490
if (fs::path(filename).extension().string() == ".splat"){
465491
saveSplat(filename);
466492
}else{
467-
savePly(filename);
493+
savePly(filename, step);
468494
}
469495
std::cout << "Wrote " << filename << std::endl;
470496
}
471497

472-
void Model::savePly(const std::string &filename){
498+
void Model::savePly(const std::string &filename, int step){
473499
std::ofstream o(filename, std::ios::binary);
474500
int numPoints = means.size(0);
475501

476502
o << "ply" << std::endl;
477503
o << "format binary_little_endian 1.0" << std::endl;
478-
o << "comment Generated by opensplat" << std::endl;
504+
o << "comment Generated by opensplat at iteration " << step << std::endl;
479505
o << "element vertex " << numPoints << std::endl;
480506
o << "property float x" << std::endl;
481507
o << "property float y" << std::endl;
@@ -564,14 +590,14 @@ void Model::saveSplat(const std::string &filename){
564590
o.close();
565591
}
566592

567-
void Model::saveDebugPly(const std::string &filename){
593+
void Model::saveDebugPly(const std::string &filename, int step){
568594
// A standard PLY
569595
std::ofstream o(filename, std::ios::binary);
570596
int numPoints = means.size(0);
571597

572598
o << "ply" << std::endl;
573599
o << "format binary_little_endian 1.0" << std::endl;
574-
o << "comment Generated by opensplat" << std::endl;
600+
o << "comment Generated by opensplat at iteration " << step << std::endl;
575601
o << "element vertex " << numPoints << std::endl;
576602
o << "property float x" << std::endl;
577603
o << "property float y" << std::endl;
@@ -593,6 +619,157 @@ void Model::saveDebugPly(const std::string &filename){
593619
std::cout << "Wrote " << filename << std::endl;
594620
}
595621

622+
int Model::loadPly(const std::string &filename){
623+
std::ifstream f(filename, std::ios::binary);
624+
if (!f.is_open()) throw std::runtime_error("Invalid PLY file");
625+
626+
// Ensure we have a valid ply file
627+
std::string line;
628+
int numPoints;
629+
int step;
630+
size_t bytesRead = 0;
631+
632+
std::getline(f, line);
633+
bytesRead += f.gcount();
634+
635+
if (line == "ply"){
636+
std::getline(f, line);
637+
bytesRead += f.gcount();
638+
if (line == "format binary_little_endian 1.0"){
639+
std::getline(f, line);
640+
bytesRead += f.gcount();
641+
const std::string pattern = "comment Generated by opensplat at iteration ";
642+
643+
if (line.rfind(pattern, 0) == 0){
644+
step = std::stoi(line.substr(pattern.length()));
645+
if (step >= 0){
646+
std::getline(f, line);
647+
bytesRead += f.gcount();
648+
const std::string pattern = "element vertex ";
649+
650+
if (line.rfind(pattern, 0) == 0){
651+
const int numPoints = std::stoi(line.substr(pattern.length()));
652+
653+
const char *requiredProps[] = {
654+
"property float x",
655+
"property float y",
656+
"property float z",
657+
"property float nx",
658+
"property float ny",
659+
"property float nz",
660+
"property float f_dc_"
661+
"property float f_rest_",
662+
"property float opacity",
663+
"property float scale_0",
664+
"property float scale_1",
665+
"property float scale_2",
666+
"property float rot_0",
667+
"property float rot_1",
668+
"property float rot_2",
669+
"property float rot_3",
670+
"end_header"
671+
};
672+
673+
for (int i = 0; i < 6; i++){
674+
std::getline(f, line);
675+
bytesRead += f.gcount();
676+
if (line != requiredProps[i]){
677+
throw std::runtime_error(std::string("PLY file's header does not contain required property: ") + requiredProps[i]);
678+
}
679+
}
680+
std::getline(f, line);
681+
bytesRead += f.gcount();
682+
683+
auto countPrefixes = [&f, &line](const char *prefix){
684+
int n = 0;
685+
while(true){
686+
if (line.rfind(prefix, 0) == 0){
687+
++n;
688+
std::getline(f, line);
689+
} else {
690+
break;
691+
}
692+
}
693+
return n;
694+
};
695+
int featuresDcSize = countPrefixes("property float f_dc_");
696+
int featuresRestSize = countPrefixes("property float f_rest_");
697+
698+
bool foundEnd = false;
699+
for (int i = 8; i < std::size(requiredProps); i++){
700+
std::getline(f, line);
701+
bytesRead += f.gcount();
702+
703+
if (line != requiredProps[i]){
704+
throw std::runtime_error(std::string("PLY file's header does not contain required property: ") + requiredProps[i]);
705+
}
706+
707+
if (line == "end_header"){
708+
foundEnd = true;
709+
break;
710+
}
711+
}
712+
713+
if (!foundEnd){
714+
throw std::runtime_error("PLY file header does not contain header end");
715+
}
716+
717+
const size_t bytesPerPoint = sizeof(float) * (14 + featuresDcSize + featuresRestSize);
718+
const size_t remainingFileSize = fs::file_size(filename) - bytesRead;
719+
if (remainingFileSize != bytesPerPoint * numPoints){
720+
std::cout << "Loading PLY..." << std::endl;
721+
722+
float zeros[3];
723+
724+
torch::Tensor meansCpu = torch::zeros({numPoints, 3}, torch::TensorOptions().dtype(torch::kFloat32));
725+
torch::Tensor featuresDcCpu = torch::zeros({numPoints, featuresDcSize}, torch::TensorOptions().dtype(torch::kFloat32));
726+
torch::Tensor featuresRestCpu = torch::zeros({numPoints, featuresRestSize}, torch::TensorOptions().dtype(torch::kFloat32));
727+
torch::Tensor opacitiesCpu = torch::zeros({numPoints, 1}, torch::TensorOptions().dtype(torch::kFloat32));
728+
torch::Tensor scalesCpu = torch::zeros({numPoints, 3}, torch::TensorOptions().dtype(torch::kFloat32));
729+
torch::Tensor quatsCpu = torch::zeros({numPoints, 4}, torch::TensorOptions().dtype(torch::kFloat32));
730+
731+
for (size_t i = 0; i < numPoints; i++){
732+
f.read(reinterpret_cast<char *>(meansCpu[i].data_ptr()), sizeof(float) * 3);
733+
f.read(reinterpret_cast<char *>(&zeros[0]), sizeof(float) * 3);
734+
f.read(reinterpret_cast<char *>(featuresDcCpu[i].data_ptr()), sizeof(float) * featuresDcSize);
735+
f.read(reinterpret_cast<char *>(featuresRestCpu[i].data_ptr()), sizeof(float) * featuresRestSize);
736+
f.read(reinterpret_cast<char *>(opacitiesCpu[i].data_ptr()), sizeof(float) * 1);
737+
f.read(reinterpret_cast<char *>(scalesCpu[i].data_ptr()), sizeof(float) * 3);
738+
f.read(reinterpret_cast<char *>(quatsCpu[i].data_ptr()), sizeof(float) * 4);
739+
}
740+
if (keepCrs){
741+
meansCpu = (meansCpu - translation) * scale;
742+
scalesCpu = torch::log(scale * torch::exp(scalesCpu));
743+
}
744+
745+
means = meansCpu.to(device).requires_grad_();
746+
featuresDc = featuresDcCpu.to(device).requires_grad_();
747+
featuresRest = featuresRestCpu.reshape({numPoints, 3, featuresRestSize/3}).transpose(2, 1).to(device).requires_grad_();
748+
opacities = opacitiesCpu.to(device).requires_grad_();
749+
scales = scalesCpu.to(device).requires_grad_();
750+
quats = quatsCpu.to(device).requires_grad_();
751+
752+
std::cerr << "Loaded " << means.size(0) << " gaussians" << std::endl;
753+
754+
setupOptimizers();
755+
756+
f.close();
757+
return step;
758+
} else {
759+
throw std::runtime_error("PLY file's data section is wrong size");
760+
}
761+
}
762+
} else {
763+
throw std::runtime_error("PLY file failed sanity check: iteration count should not begin at 0");
764+
}
765+
} else if (line.rfind("comment Generated by opensplat")){
766+
throw std::runtime_error("PLY file does not contain iteration count metadata. You can edit the file to add this metadata manually, by changing \"comment Generated by opensplat\" to \"comment Generated by opensplat at iteration 12345\", changing 12345 to the desired value.");
767+
}
768+
}
769+
}
770+
throw std::runtime_error("Invalid PLY file");
771+
}
772+
596773
torch::Tensor Model::mainLoss(torch::Tensor &rgb, torch::Tensor &gt, float ssimWeight){
597774
torch::Tensor ssimLoss = 1.0f - ssim.eval(rgb, gt);
598775
torch::Tensor l1Loss = l1(rgb, gt);

0 commit comments

Comments
 (0)