diff --git a/include/neural-graphics-primitives/testbed.h b/include/neural-graphics-primitives/testbed.h index 0edbf9fda724f797d78fe584374203bc841a16e1..4514162b062e06a4d568d2ef88b7ceafd3315226 100644 --- a/include/neural-graphics-primitives/testbed.h +++ b/include/neural-graphics-primitives/testbed.h @@ -70,6 +70,7 @@ public: bool clear_tmp_dir(); void load_training_data(const std::string& data_path); + void reload_training_data(); void clear_training_data(); void set_mode(ETestbedMode mode); diff --git a/src/testbed.cu b/src/testbed.cu index 119b011f2f7691188b2f30f2d6eec883c9636d7e..ab674113ae56b85086f29c869376775e7bd493e6 100644 --- a/src/testbed.cu +++ b/src/testbed.cu @@ -99,6 +99,32 @@ json merge_parent_network_config(const json &child, const fs::path &child_filena return parent; } +std::string get_filename_in_data_path_with_suffix(fs::path data_path, fs::path network_config_path, const char* suffix) { + // use the network config name along with the data path to build a filename with the requested suffix & extension + std::string default_name = network_config_path.basename(); + if (default_name == "") { + default_name = "base"; + } + + if (data_path.empty()) { + return default_name + std::string(suffix); + } + + if (data_path.is_directory()) { + return (data_path / (default_name + std::string{suffix})).str(); + } + + return data_path.stem().str() + "_" + default_name + std::string(suffix); +} + +void Testbed::update_imgui_paths() { + snprintf(m_imgui.cam_path_path, sizeof(m_imgui.cam_path_path), "%s", get_filename_in_data_path_with_suffix(m_data_path, m_network_config_path, "_cam.json").c_str()); + snprintf(m_imgui.extrinsics_path, sizeof(m_imgui.extrinsics_path), "%s", get_filename_in_data_path_with_suffix(m_data_path, m_network_config_path, "_extrinsics.json").c_str()); + snprintf(m_imgui.mesh_path, sizeof(m_imgui.mesh_path), "%s", get_filename_in_data_path_with_suffix(m_data_path, m_network_config_path, ".obj").c_str()); + snprintf(m_imgui.snapshot_path, sizeof(m_imgui.snapshot_path), "%s", get_filename_in_data_path_with_suffix(m_data_path, m_network_config_path, ".msgpack").c_str()); + snprintf(m_imgui.video_path, sizeof(m_imgui.video_path), "%s", get_filename_in_data_path_with_suffix(m_data_path, m_network_config_path, "_video.mp4").c_str()); +} + void Testbed::load_training_data(const std::string& data_path_str) { fs::path data_path = data_path_str; if (!data_path.exists()) { @@ -115,19 +141,23 @@ void Testbed::load_training_data(const std::string& data_path_str) { m_data_path = data_path; - if (!m_data_path.exists()) { - throw std::runtime_error{fmt::format("Data path '{}' does not exist.", m_data_path.str())}; - } - switch (m_testbed_mode) { - case ETestbedMode::Nerf: load_nerf(); break; - case ETestbedMode::Sdf: load_mesh(); break; - case ETestbedMode::Image: load_image(); break; - case ETestbedMode::Volume: load_volume(); break; + case ETestbedMode::Nerf: load_nerf(data_path); break; + case ETestbedMode::Sdf: load_mesh(data_path); break; + case ETestbedMode::Image: load_image(data_path); break; + case ETestbedMode::Volume: load_volume(data_path); break; default: throw std::runtime_error{"Invalid testbed mode."}; } m_training_data_available = true; + + update_imgui_paths(); +} + +void Testbed::reload_training_data() { + if (m_data_path.exists()) { + load_training_data(m_data_path.str()); + } } void Testbed::clear_training_data() { @@ -1511,6 +1541,7 @@ bool Testbed::keyboard_event() { } } + bool ctrl = ImGui::GetIO().KeyMods & ImGuiKeyModFlags_Ctrl; bool shift = ImGui::GetIO().KeyMods & ImGuiKeyModFlags_Shift; if (ImGui::IsKeyPressed('Z')) { @@ -1530,6 +1561,15 @@ bool Testbed::keyboard_event() { if (shift) { reset_camera(); } else { + if (ctrl) { + reload_training_data(); + // After reloading the training data, also reset the NN. + // Presumably, there is no use case where the user would + // like to hot-reload the same training data set other than + // to slightly tweak its parameters. And to observe that + // effect meaningfully, the NN should be trained from scratch. + } + reload_network_from_file(); } } diff --git a/src/testbed_image.cu b/src/testbed_image.cu index 742a3b1784c3d9c670b4d2cd43069dbcc96bb627..0b8313f72ec97f6fdfd41e272a5b1c0e9183be98 100644 --- a/src/testbed_image.cu +++ b/src/testbed_image.cu @@ -27,6 +27,7 @@ using namespace Eigen; using namespace tcnn; +namespace fs = filesystem; NGP_NAMESPACE_BEGIN @@ -359,53 +360,44 @@ void Testbed::render_image(CudaRenderBuffer& render_buffer, cudaStream_t stream) ); } -void Testbed::load_image() { - if (equals_case_insensitive(m_data_path.extension(), "exr")) { - load_exr_image(); - } else if (equals_case_insensitive(m_data_path.extension(), "bin")) { - load_binary_image(); +void Testbed::load_image(const fs::path& data_path) { + if (equals_case_insensitive(data_path.extension(), "exr")) { + load_exr_image(data_path); + } else if (equals_case_insensitive(data_path.extension(), "bin")) { + load_binary_image(data_path); } else { - load_stbi_image(); + load_stbi_image(data_path); } -#ifdef COLOR_SPACE_CONVERT - const dim3 threads = { 32, 32, 1 }; - const dim3 blocks = { div_round_up((uint32_t)m_image.resolution.x(), threads.x), div_round_up((uint32_t)m_image.resolution.x(), threads.y), 1 }; - if (m_image.type == EDataType::Half) - colorspace_convert_image_half<<<blocks, threads, 0>>>(m_image.resolution, m_image.data.data()); - else - colorspace_convert_image_float<<<blocks, threads, 0>>>(m_image.resolution, m_image.data.data()); -#endif - tlog::success() << "Loaded a " << (m_image.type == EDataType::Half ? "half" : "full") << "-precision image with " << m_image.resolution.x() << "x" << m_image.resolution.y() << " pixels."; } -void Testbed::load_exr_image() { - if (!m_data_path.exists()) { - throw std::runtime_error{fmt::format("Image file '{}' does not exist.", m_data_path.str())}; +void Testbed::load_exr_image(const fs::path& data_path) { + if (!data_path.exists()) { + throw std::runtime_error{fmt::format("Image file '{}' does not exist.", data_path.str())}; } - tlog::info() << "Loading EXR image from " << m_data_path; + tlog::info() << "Loading EXR image from " << data_path; // First step: load an image that we'd like to learn - GPUMemory<float> image = load_exr(m_data_path.str(), m_image.resolution.x(), m_image.resolution.y()); + GPUMemory<float> image = load_exr(data_path.str(), m_image.resolution.x(), m_image.resolution.y()); m_image.data.resize(image.size() * sizeof(float)); CUDA_CHECK_THROW(cudaMemcpy(m_image.data.data(), image.data(), image.size() * sizeof(float), cudaMemcpyDeviceToDevice)); m_image.type = EDataType::Float; } -void Testbed::load_stbi_image() { - if (!m_data_path.exists()) { - throw std::runtime_error{fmt::format("Image file '{}' does not exist.", m_data_path.str())}; +void Testbed::load_stbi_image(const fs::path& data_path) { + if (!data_path.exists()) { + throw std::runtime_error{fmt::format("Image file '{}' does not exist.", data_path.str())}; } - tlog::info() << "Loading STBI image from " << m_data_path; + tlog::info() << "Loading STBI image from " << data_path; // First step: load an image that we'd like to learn - GPUMemory<float> image = load_stbi(m_data_path.str(), m_image.resolution.x(), m_image.resolution.y()); + GPUMemory<float> image = load_stbi(data_path.str(), m_image.resolution.x(), m_image.resolution.y()); m_image.data.resize(image.size() * sizeof(float)); CUDA_CHECK_THROW(cudaMemcpy(m_image.data.data(), image.data(), image.size() * sizeof(float), cudaMemcpyDeviceToDevice)); @@ -413,14 +405,14 @@ void Testbed::load_stbi_image() { } -void Testbed::load_binary_image() { - if (!m_data_path.exists()) { - throw std::runtime_error{fmt::format("Image file '{}' does not exist.", m_data_path.str())}; +void Testbed::load_binary_image(const fs::path& data_path) { + if (!data_path.exists()) { + throw std::runtime_error{fmt::format("Image file '{}' does not exist.", data_path.str())}; } - tlog::info() << "Loading binary image from " << m_data_path; + tlog::info() << "Loading binary image from " << data_path; - std::ifstream f(m_data_path.str(), std::ios::in | std::ios::binary); + std::ifstream f(data_path.str(), std::ios::in | std::ios::binary); f.read(reinterpret_cast<char*>(&m_image.resolution.y()), sizeof(int)); f.read(reinterpret_cast<char*>(&m_image.resolution.x()), sizeof(int)); diff --git a/src/testbed_nerf.cu b/src/testbed_nerf.cu index 1b4624dc42965797f41327e59aff923b35c20de1..cc054f828d981e1efae0f85f3ec1bc07bbe14f7f 100644 --- a/src/testbed_nerf.cu +++ b/src/testbed_nerf.cu @@ -2546,8 +2546,9 @@ void Testbed::Nerf::Training::update_transforms(int first, int last) { void Testbed::create_empty_nerf_dataset(size_t n_images, int aabb_scale, bool is_hdr) { m_data_path = {}; + set_mode(ETestbedMode::Nerf); m_nerf.training.dataset = ngp::create_empty_nerf_dataset(n_images, aabb_scale, is_hdr); - load_nerf(); + load_nerf(m_data_path); m_nerf.training.n_images_for_training = 0; m_training_data_available = true; } @@ -2644,21 +2645,21 @@ void Testbed::load_nerf_post() { // moved the second half of load_nerf here m_up_dir = m_nerf.training.dataset.up; } -void Testbed::load_nerf() { - if (!m_data_path.empty()) { +void Testbed::load_nerf(const fs::path& data_path) { + if (!data_path.empty()) { std::vector<fs::path> json_paths; - if (m_data_path.is_directory()) { - for (const auto& path : fs::directory{m_data_path}) { + if (data_path.is_directory()) { + for (const auto& path : fs::directory{data_path}) { if (path.is_file() && equals_case_insensitive(path.extension(), "json")) { json_paths.emplace_back(path); } } - } else if (equals_case_insensitive(m_data_path.extension(), "msgpack")) { - load_snapshot(m_data_path.str()); + } else if (equals_case_insensitive(data_path.extension(), "msgpack")) { + load_snapshot(data_path.str()); set_train(false); return; - } else if (equals_case_insensitive(m_data_path.extension(), "json")) { - json_paths.emplace_back(m_data_path); + } else if (equals_case_insensitive(data_path.extension(), "json")) { + json_paths.emplace_back(data_path); } else { throw std::runtime_error{"NeRF data path must either be a json file or a directory containing json files."}; } diff --git a/src/testbed_sdf.cu b/src/testbed_sdf.cu index 5a0065e8874545fddc6783ff3b0aa6dc057182d8..df98fa5ba161839b5112c6d624ae1463324111ff 100644 --- a/src/testbed_sdf.cu +++ b/src/testbed_sdf.cu @@ -32,6 +32,7 @@ using namespace Eigen; using namespace tcnn; +namespace fs = filesystem; NGP_NAMESPACE_BEGIN @@ -1012,42 +1013,59 @@ void Testbed::render_sdf( for (uint32_t i = 0; i < n_hit; ++i) { total_n_steps += payloads_final_cpu[i].n_steps; } + tlog::info() << "Total steps per hit= " << total_n_steps << "/" << n_hit << " = " << ((float)total_n_steps/(float)n_hit); } } -void Testbed::load_mesh() { +std::vector<Vector3f> load_stl(const std::string& filename) { std::vector<Vector3f> vertices; - if (equals_case_insensitive(m_data_path.extension(), "obj")) { - vertices = load_obj(m_data_path.str()); - } else if (equals_case_insensitive(m_data_path.extension(), "stl")) { - FILE* f = fopen(m_data_path.str().c_str(), "rb"); - if (!f) { - throw std::runtime_error{"stl file not found"}; - } - uint32_t buf[21]={}; - if (fread(buf, 4, 21, f) != 4*21) { - throw std::runtime_error{"stl file too small for header"}; - } - uint32_t nfaces = buf[20]; - if (memcmp(buf,"solid",5)==0 || buf[20]==0) { - fclose(f); - throw std::runtime_error{"ascii stl files are not supported"}; - } - vertices.reserve(nfaces * 3); - for (uint32_t i = 0; i < nfaces; ++i) { - if (fread(buf, 1, 50, f) < 50) { - nfaces = i; - break; - } - vertices.push_back(*(Vector3f*)(buf + 3)); - vertices.push_back(*(Vector3f*)(buf + 6)); - vertices.push_back(*(Vector3f*)(buf + 9)); + + std::ifstream f{filename, std::ios::in | std::ios::binary}; + if (!f) { + throw std::runtime_error{fmt::format("Mesh file '{}' not found", filename)}; + } + + uint32_t buf[21] = {}; + f.read((char*)buf, 4 * 21); + if (f.gcount() < 4 * 21) { + throw std::runtime_error{fmt::format("Mesh file '{}' too small for STL header", filename)}; + } + + uint32_t nfaces = buf[20]; + if (memcmp(buf, "solid", 5) == 0 || buf[20] == 0) { + throw std::runtime_error{fmt::format("ASCII STL file '{}' not supported", filename)}; + } + + vertices.reserve(nfaces * 3); + for (uint32_t i = 0; i < nfaces; ++i) { + f.read((char*)buf, 50); + if (f.gcount() < 50) { + nfaces = i; + break; } - fclose(f); + + vertices.push_back(*(Vector3f*)(buf + 3)); + vertices.push_back(*(Vector3f*)(buf + 6)); + vertices.push_back(*(Vector3f*)(buf + 9)); + } + + return vertices; +} + +void Testbed::load_mesh(const fs::path& data_path) { + tlog::info() << "Loading mesh from '" << data_path << "'"; + auto start = std::chrono::steady_clock::now(); + + std::vector<Vector3f> vertices; + if (equals_case_insensitive(data_path.extension(), "obj")) { + vertices = load_obj(data_path.str()); + } else if (equals_case_insensitive(data_path.extension(), "stl")) { + vertices = load_stl(data_path.str()); } else { - throw std::runtime_error{"Sdf data path must be a mesh in ascii .obj or binary .stl format."}; + throw std::runtime_error{"SDF data path must be a mesh in ascii .obj or binary .stl format."}; } + // The expected format is // [v1.x][v1.y][v1.z][v2.x]... size_t n_vertices = vertices.size(); @@ -1076,6 +1094,7 @@ void Testbed::load_mesh() { for (size_t i = 0; i < n_vertices; ++i) { m_aabb.enlarge(vertices[i]); } + m_aabb.inflate(m_aabb.diag().norm() * inflation); m_aabb = m_aabb.intersection(BoundingBox{Vector3f::Zero(), Vector3f::Ones()}); m_render_aabb = m_aabb; @@ -1087,8 +1106,10 @@ void Testbed::load_mesh() { m_sdf.triangles_cpu[i/3] = {vertices[i+0], vertices[i+1], vertices[i+2]}; } - if (!m_sdf.triangle_bvh) + if (!m_sdf.triangle_bvh) { m_sdf.triangle_bvh = TriangleBvh::make(); + } + m_sdf.triangle_bvh->build(m_sdf.triangles_cpu, 8); m_sdf.triangles_gpu.resize_and_copy_from_host(m_sdf.triangles_cpu); m_sdf.triangle_bvh->build_optix(m_sdf.triangles_gpu, m_stream.get()); @@ -1116,7 +1137,8 @@ void Testbed::load_mesh() { m_sdf.training.idx = 0; m_sdf.training.size = 0; - tlog::success() << "Loaded mesh: triangles=" << n_triangles << " AABB=" << m_raw_aabb << " after scaling=" << m_aabb; + tlog::success() << "Loaded mesh after " << tlog::durationToString(std::chrono::steady_clock::now() - start); + tlog::info() << " n_triangles=" << n_triangles << " aabb=" << m_raw_aabb; } void Testbed::generate_training_samples_sdf(Vector3f* positions, float* distances, uint32_t n_to_generate, cudaStream_t stream, bool uniform_only) { diff --git a/src/testbed_volume.cu b/src/testbed_volume.cu index 6559f08b0ac9b4a255f6b0f1d31f6f90b538160b..168b666fb6b063f26a81d7d9dc9a84d9515f7581 100644 --- a/src/testbed_volume.cu +++ b/src/testbed_volume.cu @@ -33,6 +33,7 @@ using namespace Eigen; using namespace tcnn; +namespace fs = filesystem; NGP_NAMESPACE_BEGIN @@ -549,12 +550,12 @@ struct NanoVDBMetaData }; static_assert(sizeof(NanoVDBMetaData) == 176, "nanovdb padding error"); -void Testbed::load_volume() { - if (!m_data_path.exists()) { - throw std::runtime_error{m_data_path.str() + " does not exist."}; +void Testbed::load_volume(const fs::path& data_path) { + if (!data_path.exists()) { + throw std::runtime_error{data_path.str() + " does not exist."}; } - tlog::info() << "Loading NanoVDB file from " << m_data_path; - std::ifstream f(m_data_path.str(), std::ios::in | std::ios::binary); + tlog::info() << "Loading NanoVDB file from " << data_path; + std::ifstream f(data_path.str(), std::ios::in | std::ios::binary); NanoVDBFileHeader header; NanoVDBMetaData metadata; f.read(reinterpret_cast<char*>(&header), sizeof(header)); @@ -584,37 +585,41 @@ void Testbed::load_volume() { m_volume.nanovdb_grid.copy_from_host(cpugrid); const nanovdb::FloatGrid* grid = reinterpret_cast<const nanovdb::FloatGrid*>(cpugrid.data()); - float mn=1e10f,mx=-1e10f; + float mn = 10000.0f, mx = -10000.0f; bool hmm = grid->hasMinMax(); //grid->tree().extrema(mn,mx); - int xsize = std::max(1,metadata.indexBBox[1][0]-metadata.indexBBox[0][0]); - int ysize = std::max(1,metadata.indexBBox[1][1]-metadata.indexBBox[0][1]); - int zsize = std::max(1,metadata.indexBBox[1][2]-metadata.indexBBox[0][2]); - float maxsize=std::max(std::max(xsize,ysize),zsize); - float scale = 1.f/maxsize; + int xsize = std::max(1, metadata.indexBBox[1][0] - metadata.indexBBox[0][0]); + int ysize = std::max(1, metadata.indexBBox[1][1] - metadata.indexBBox[0][1]); + int zsize = std::max(1, metadata.indexBBox[1][2] - metadata.indexBBox[0][2]); + float maxsize = std::max(std::max(xsize, ysize), zsize); + float scale = 1.0f / maxsize; m_aabb = m_render_aabb = BoundingBox{ - Vector3f{0.5f-xsize*scale*0.5f,0.5f-ysize*scale*0.5f,0.5f-zsize*scale*0.5f}, - Vector3f{0.5f+xsize*scale*0.5f,0.5f+ysize*scale*0.5f,0.5f+zsize*scale*0.5f} + Vector3f{0.5f - xsize * scale * 0.5f, 0.5f - ysize * scale * 0.5f, 0.5f - zsize * scale * 0.5f}, + Vector3f{0.5f + xsize * scale * 0.5f, 0.5f + ysize * scale * 0.5f, 0.5f + zsize * scale * 0.5f}, }; m_volume.world2index_scale = maxsize; - m_volume.world2index_offset= Vector3f{(metadata.indexBBox[0][0]+metadata.indexBBox[1][0])*0.5f-0.5f*maxsize,(metadata.indexBBox[0][1]+metadata.indexBBox[1][1])*0.5f-0.5f*maxsize,(metadata.indexBBox[0][2]+metadata.indexBBox[1][2])*0.5f-0.5f*maxsize}; + m_volume.world2index_offset = Vector3f{ + (metadata.indexBBox[0][0] + metadata.indexBBox[1][0]) * 0.5f - 0.5f * maxsize, + (metadata.indexBBox[0][1] + metadata.indexBBox[1][1]) * 0.5f - 0.5f * maxsize, + (metadata.indexBBox[0][2] + metadata.indexBBox[1][2]) * 0.5f - 0.5f * maxsize, + }; auto acc = grid->tree().getAccessor(); std::vector<uint8_t> bitgrid; - bitgrid.resize(128*128*128/8); - for (int i=metadata.indexBBox[0][0];i<metadata.indexBBox[1][0];++i) - for (int j=metadata.indexBBox[0][1];j<metadata.indexBBox[1][1];++j) - for (int k=metadata.indexBBox[0][2];k<metadata.indexBBox[1][2];++k) { - float d = acc.getValue({i,j,k}); - if (d>mx) mx=d; - if (d<mn) mn=d; - if (d>0.001f) { - float fx=((i+0.5f)-m_volume.world2index_offset.x())/m_volume.world2index_scale; - float fy=((j+0.5f)-m_volume.world2index_offset.y())/m_volume.world2index_scale; - float fz=((k+0.5f)-m_volume.world2index_offset.z())/m_volume.world2index_scale; - uint32_t bitidx = tcnn::morton3D(int(fx*128.f+0.5f),int(fy*128.f+0.5f),int(fz*128.f+0.5f)); - if (bitidx<128*128*128) - bitgrid[bitidx/8]|=1<<(bitidx&7); + bitgrid.resize(128 * 128 * 128 / 8); + for (int i = metadata.indexBBox[0][0]; i < metadata.indexBBox[1][0]; ++i) + for (int j = metadata.indexBBox[0][1]; j < metadata.indexBBox[1][1]; ++j) + for (int k = metadata.indexBBox[0][2]; k < metadata.indexBBox[1][2]; ++k) { + float d = acc.getValue({i, j, k}); + if (d > mx) mx = d; + if (d < mn) mn = d; + if (d > 0.001f) { + float fx = ((i + 0.5f) - m_volume.world2index_offset.x()) / m_volume.world2index_scale; + float fy = ((j + 0.5f) - m_volume.world2index_offset.y()) / m_volume.world2index_scale; + float fz = ((k + 0.5f) - m_volume.world2index_offset.z()) / m_volume.world2index_scale; + uint32_t bitidx = tcnn::morton3D(int(fx * 128.0f + 0.5f), int(fy * 128.0f + 0.5f), int(fz * 128.0f + 0.5f)); + if (bitidx < 128 * 128 * 128) + bitgrid[bitidx / 8] |= 1 << (bitidx & 7); } } m_volume.bitgrid.enlarge(bitgrid.size());