mirror of
https://github.com/harry7557558/spirula-studio.git
synced 2026-10-02 02:44:54 +08:00
add dataset centering option
This commit is contained in:
@@ -315,6 +315,28 @@ viewer's default camera, not the training coordinates. The unported methods
|
||||
(`pca`, `vertical`, `gsplat`, `focus`) still have a working Python reference:
|
||||
[notes/pose-normalization.md](notes/pose-normalization.md).
|
||||
|
||||
`--scene-center` is the one option that DOES move the training coordinates:
|
||||
it translates every camera and seed point so the chosen statistic sits at the
|
||||
origin, in double precision, before anything is narrowed to float. That is
|
||||
what a geo-referenced reconstruction needs -- a model millions of units from
|
||||
its origin loses metres to single precision otherwise. The modes are
|
||||
`point-median` (geometric median of the seed cloud), `camera-median`,
|
||||
`camera-focus` (the point the optical axes converge on), `point-mean` and
|
||||
`camera-mean`; `none` (the default) keeps the frame the files came in. The
|
||||
shift, and the identity rotation and scale that go with it, are written to
|
||||
`scene_transform.json` in the run folder in every common spelling (4x4
|
||||
matrices, quaternions, Euler angles), so a downstream tool can put the
|
||||
splats back into the dataset's frame without converting anything by hand.
|
||||
The centre is taken over every frame before the train/eval split and over
|
||||
the whole seed cloud, so both splits, `spirula mesh` and the viewers -- all
|
||||
of which re-read `config.json` -- land in the same frame.
|
||||
|
||||
The same six modes are also a *view* setting, offered by all three viewers as
|
||||
a "center" menu (camera position median by default) that moves the orbit
|
||||
pivot and nothing else. `src/data/SceneCenter.h` is the one implementation;
|
||||
the table of centres travels to a viewer on `ViewerRenderConfig::centers`,
|
||||
and the training viewer's browser client fetches it from `/scene`.
|
||||
|
||||
## Train/eval split
|
||||
|
||||
`eval_mode` selects the strategy:
|
||||
|
||||
@@ -542,8 +542,13 @@ switch off: the parsers rotate every dataset so the mean camera up axis
|
||||
becomes +Z, which on the X5 walk is 11.4 deg from the IMU's answer, and that
|
||||
rotation is what turntable and first-person navigation orbit about. The switch
|
||||
is offered wherever the rotation is not the identity, and undoes only that
|
||||
rotation — an `applied_transform` a file came with is left alone. The second
|
||||
bit puts the grid's cell size on screen as a length, so a metric model can be
|
||||
rotation — an `applied_transform` a file came with is left alone. Next to it
|
||||
sits the **center** menu, which picks what the view orbits about (camera
|
||||
position median by default; `src/data/SceneCenter.h`) and moves only the
|
||||
camera. All three viewers offer it: the native viewport reads the table off
|
||||
`ViewerRenderConfig`, and the browser clients ask for it — `/scene` for the
|
||||
training viewer, `ssv_ds_fit_sphere` for the standalone one. The second bit
|
||||
puts the grid's cell size on screen as a length, so a metric model can be
|
||||
measured by looking at it.
|
||||
|
||||
The GUI also stopped hiding the inputs. Each video row says whether the file
|
||||
|
||||
+35
-14
@@ -1,6 +1,7 @@
|
||||
// TrainerCore.cpp -- see TrainerCore.h.
|
||||
|
||||
#include "app/TrainerCore.h"
|
||||
#include "data/SceneTransform.h"
|
||||
#include "app/EvalMetrics.h"
|
||||
#include "checkpoint/Adapt.h"
|
||||
#include "checkpoint/Resume.h"
|
||||
@@ -223,7 +224,7 @@ SeedSplats seed_splats(const ColmapPoints3D& pts, const TrainConfig& cfg,
|
||||
float rescale = cfg.relative_scale.value_or(1.0f);
|
||||
for (int64_t i = 0; i < num; i++)
|
||||
for (int d = 0; d < 3; d++)
|
||||
s.means[i*3 + d] = pts.xyz[pick[i]*3 + d] * rescale;
|
||||
s.means[i*3 + d] = (float)(pts.xyz[pick[i]*3 + d] * rescale);
|
||||
|
||||
// log(scale_init * sqrt(mean d^2 of 4-NN)) over xyz, over the DISTINCT
|
||||
// points: a repeat is its own zero-distance neighbor, and would seed
|
||||
@@ -509,18 +510,26 @@ EngineStepConfig build_step_config(const TrainConfig& c, const RunState& st, int
|
||||
// config.json dump
|
||||
// ===========================================================================
|
||||
|
||||
// Flat dump: one key per flag, spelled exactly as the flag is. It used to be
|
||||
// nested under the field table's group column, which quietly made a
|
||||
// presentational choice part of an on-disk format -- moving a flag to another
|
||||
// heading moved its key, and a reader that could not find it silently fell
|
||||
// back to the default. Flat, a heading can be renamed or reshuffled without
|
||||
// touching anything that reads this file (`spirula mesh`, --resume).
|
||||
//
|
||||
// Macro flags (--quality and friends) are written alongside the values they
|
||||
// resolved to, so a reader takes the values and never re-resolves them.
|
||||
//
|
||||
// The encoding itself is config/TrainConfigJson.h, shared with --resume's
|
||||
// reader and the GUI's saved presets so the three cannot disagree.
|
||||
// p_train = relative_scale * (p_dataset - center): the parser's shift and the
|
||||
// trainer's own rescale, which is every way the splats' frame differs from
|
||||
// the dataset's.
|
||||
void save_scene_transform_json(const ParsedDataset& ds, const TrainConfig& c,
|
||||
const fs::path& out_dir) {
|
||||
SceneTransform T;
|
||||
T.scale = (double)c.relative_scale.value_or(1.0f);
|
||||
for (int i = 0; i < 3; i++) T.t[i] = -T.scale * ds.center[i];
|
||||
const std::string text =
|
||||
scene_transform_json(T, ds.center_mode, ds.center.data());
|
||||
const fs::path path = out_dir / "scene_transform.json";
|
||||
FILE* f = std::fopen(path.string().c_str(), "w");
|
||||
if (!f) throw std::runtime_error("cannot write " + path.string());
|
||||
std::fputs(text.c_str(), f);
|
||||
std::fclose(f);
|
||||
}
|
||||
|
||||
// Flat, one key per flag: a key that followed the field table's heading
|
||||
// moved whenever a flag was reshuffled, and readers fell back to the default.
|
||||
// Macro flags are written beside what they resolved to; config/TrainConfigJson.h.
|
||||
void save_config_json(const TrainConfig& c, const fs::path& out_dir,
|
||||
const std::string& preset) {
|
||||
FILE* f = std::fopen((out_dir / "config.json").string().c_str(), "w");
|
||||
@@ -602,6 +611,7 @@ void TrainerSession::load_dataset() {
|
||||
pcfg.eval_interval = cfg.eval_interval;
|
||||
pcfg.train_split_fraction = cfg.train_split_fraction;
|
||||
pcfg.outlier_threshold = cfg.outlier_threshold;
|
||||
pcfg.center_mode = cfg.scene_center;
|
||||
pcfg.probe_image_size = probe_image_size;
|
||||
pcfg.train_resolution_divisor = cfg.train_resolution_divisor;
|
||||
pcfg.downscale_rounding_mode = cfg.downscale_rounding_mode;
|
||||
@@ -609,6 +619,12 @@ void TrainerSession::load_dataset() {
|
||||
pcfg.metashape_ply = cfg.metashape_ply;
|
||||
pcfg.metashape_psx = cfg.metashape_psx;
|
||||
ds = parse_dataset(cfg.data, pcfg, cfg.data_format);
|
||||
if (ds.center_mode != "none") {
|
||||
char xyz[96];
|
||||
std::snprintf(xyz, sizeof xyz, "%.12g, %.12g, %.12g",
|
||||
ds.center[0], ds.center[1], ds.center[2]);
|
||||
log(lfmt(lmsg::scene_centered, {ds.center_mode, xyz}));
|
||||
}
|
||||
|
||||
// An EXR carries its own colour space, and nothing downstream can recover
|
||||
// it: DataManager hands the engine the file's raw scene-linear floats. The
|
||||
@@ -768,8 +784,10 @@ void TrainerSession::setup_engine() {
|
||||
(fs::path(cfg.data).stem().string() + "_" + stamp);
|
||||
}
|
||||
fs::create_directories(out_dir);
|
||||
if (write_config_json)
|
||||
if (write_config_json) {
|
||||
save_config_json(cfg, out_dir, preset);
|
||||
save_scene_transform_json(ds, cfg, out_dir);
|
||||
}
|
||||
log(lfmt(lmsg::output_directory, {fs::absolute(out_dir).string()}));
|
||||
|
||||
// ---- Engine setup -------------------------------------------------
|
||||
@@ -1221,6 +1239,8 @@ ViewerRenderConfig TrainerSession::make_viewer_config() const {
|
||||
cfg.normal_distortion_reg != 0.0f;
|
||||
const auto color = resolve_color(cfg);
|
||||
vc.color_space_on = color.splat_on();
|
||||
vc.centers = dsparse::scene_centers(ds);
|
||||
vc.center_cameras = ds.num_cameras > 0;
|
||||
vc.train_frame_scale = ds.train_frame_scale;
|
||||
vc.train_to_normalized = ds.train_to_normalized;
|
||||
vc.base_camera_size = viewer_base_camera_size;
|
||||
@@ -1279,6 +1299,7 @@ void TrainerSession::eval() {
|
||||
pcfg.eval_interval = cfg.eval_interval;
|
||||
pcfg.train_split_fraction = cfg.train_split_fraction;
|
||||
pcfg.outlier_threshold = cfg.outlier_threshold;
|
||||
pcfg.center_mode = cfg.scene_center;
|
||||
pcfg.probe_image_size = probe_image_size;
|
||||
pcfg.train_resolution_divisor = cfg.train_resolution_divisor;
|
||||
pcfg.downscale_rounding_mode = cfg.downscale_rounding_mode;
|
||||
|
||||
@@ -125,6 +125,10 @@ EngineStepConfig build_step_config(const TrainConfig& c, const RunState& st,
|
||||
// Flat config.json dump, one key per flag (config/TrainConfigJson.h).
|
||||
void save_config_json(const TrainConfig& c, const std::filesystem::path& out_dir,
|
||||
const std::string& preset);
|
||||
// scene_transform.json: the similarity from the dataset's own frame to the
|
||||
// one the splats are trained in (data/SceneTransform.h).
|
||||
void save_scene_transform_json(const ParsedDataset& ds, const TrainConfig& c,
|
||||
const std::filesystem::path& out_dir);
|
||||
|
||||
// "" when this config is runnable, else the sentence naming the flag that is
|
||||
// not implemented -- exactly what TrainerSession::check_config() throws. A
|
||||
|
||||
@@ -76,6 +76,7 @@ MeshCameras load_cameras(const JsonValue& run_cfg, const std::string& data_dir,
|
||||
pcfg.metashape_ply = dp_str("metashape_ply", "");
|
||||
pcfg.metashape_psx = dp_str("metashape_psx", "");
|
||||
pcfg.downscale_rounding_mode = dp_str("downscale_rounding_mode", "floor");
|
||||
pcfg.center_mode = dp_str("scene_center", "none");
|
||||
pcfg.probe_image_size = probe_image_size;
|
||||
{
|
||||
const JsonValue* v = run_cfg.find("train_resolution_divisor");
|
||||
|
||||
@@ -718,7 +718,7 @@ bool PreviewRenderer::build(const ParsedDataset& ds, const PostSplitCameras& pos
|
||||
for (int i = 0; i < 16; i++) T[i] = ds.train_to_normalized[i];
|
||||
dsparse::invert_affine4x4(T, A);
|
||||
}
|
||||
auto map_pt = [&](const float p[3], float out[3]) {
|
||||
auto map_pt = [&](const auto* p, float out[3]) {
|
||||
for (int r = 0; r < 3; r++)
|
||||
out[r] = (float)(A[r*4+0]*p[0] + A[r*4+1]*p[1] + A[r*4+2]*p[2] + A[r*4+3]);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "app/gui/SfmInProcess.h"
|
||||
|
||||
#ifdef SS_TOOL_SFM
|
||||
#include "sfm/Pipeline.h"
|
||||
#include "sfm/core/Cancel.h"
|
||||
#include "sfm/core/Events.h"
|
||||
@@ -9,9 +10,30 @@
|
||||
#include "sfm/core/Progress.h"
|
||||
|
||||
#include "i18n/catalog/Sfm.h"
|
||||
#else
|
||||
#include "i18n/catalog/Log.h"
|
||||
#endif
|
||||
|
||||
namespace gui {
|
||||
|
||||
#ifndef SS_TOOL_SFM
|
||||
|
||||
// SfmRunner::availability() refuses a run long before this, so reaching it is
|
||||
// a bug rather than a configuration; it still says which.
|
||||
InProcessResult run_sfm_in_process(
|
||||
const std::vector<std::string>& args,
|
||||
const std::function<void(const std::string&)>& log,
|
||||
const std::function<void(const RunStatus&)>& on_status,
|
||||
const std::atomic<bool>& cancel) {
|
||||
(void)args; (void)log; (void)on_status; (void)cancel;
|
||||
InProcessResult out;
|
||||
out.exit_code = -1;
|
||||
out.error = spirula::i18n::msg::log::err_no_sfm_module.get();
|
||||
return out;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
namespace {
|
||||
|
||||
// The line the CLI would have printed, so the GUI's log reads the same
|
||||
@@ -127,4 +149,6 @@ InProcessResult run_sfm_in_process(
|
||||
return out;
|
||||
}
|
||||
|
||||
#endif // SS_TOOL_SFM
|
||||
|
||||
} // namespace gui
|
||||
|
||||
@@ -502,11 +502,13 @@ std::vector<std::string> SfmRunner::recon_args(const SfmJob& job,
|
||||
// Not flags any more: the groups go in the manifest. Its text joins the
|
||||
// stamp so that changing a lens still counts as a different model
|
||||
// (recon_stamp_change), which is the whole point of this list.
|
||||
#ifdef SS_TOOL_SFM
|
||||
const std::string manifest = sfm::manifest_write(build_manifest(job, prep));
|
||||
if (!manifest.empty()) {
|
||||
argv.push_back("--manifest");
|
||||
argv.push_back(manifest);
|
||||
}
|
||||
#endif
|
||||
if (job.max_features > 0) {
|
||||
// Each frontend has its own count flag: the budgets are not comparable,
|
||||
// a learned detector emitting a few thousand better-localized points
|
||||
|
||||
@@ -37,7 +37,8 @@ TorchTensorView tv(std::vector<float>& v, std::vector<int64_t> shape) {
|
||||
// viewer a kilometre away looking at a dot -- and the median is what is left.
|
||||
// `radius` comes back as the web viewer's model radius, twice the MEDIAN
|
||||
// distance from the centre.
|
||||
void scene_extent(const std::vector<float>& xyz, int64_t n,
|
||||
template <typename T>
|
||||
void scene_extent(const std::vector<T>& xyz, int64_t n,
|
||||
float center[3], float& radius) {
|
||||
center[0] = center[1] = center[2] = 0.0f;
|
||||
radius = 1.0f;
|
||||
@@ -49,15 +50,15 @@ void scene_extent(const std::vector<float>& xyz, int64_t n,
|
||||
tmp.reserve((size_t)(n / step + 1));
|
||||
for (int d = 0; d < 3; d++) {
|
||||
tmp.clear();
|
||||
for (int64_t i = 0; i < n; i += step) tmp.push_back(xyz[(size_t)i * 3 + d]);
|
||||
for (int64_t i = 0; i < n; i += step) tmp.push_back((float)xyz[(size_t)i * 3 + d]);
|
||||
std::nth_element(tmp.begin(), tmp.begin() + tmp.size() / 2, tmp.end());
|
||||
center[d] = tmp[tmp.size() / 2];
|
||||
}
|
||||
tmp.clear();
|
||||
for (int64_t i = 0; i < n; i += step) {
|
||||
const float dx = xyz[(size_t)i * 3 + 0] - center[0];
|
||||
const float dy = xyz[(size_t)i * 3 + 1] - center[1];
|
||||
const float dz = xyz[(size_t)i * 3 + 2] - center[2];
|
||||
const float dx = (float)xyz[(size_t)i * 3 + 0] - center[0];
|
||||
const float dy = (float)xyz[(size_t)i * 3 + 1] - center[1];
|
||||
const float dz = (float)xyz[(size_t)i * 3 + 2] - center[2];
|
||||
tmp.push_back(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
std::nth_element(tmp.begin(), tmp.begin() + tmp.size() / 2, tmp.end());
|
||||
@@ -354,6 +355,14 @@ void SplatViewer::run(std::string path) {
|
||||
0, 0, 0, 1};
|
||||
vc.base_camera_size = 0.0f; // no cameras to draw
|
||||
vc.scene_slot = _pending_slot;
|
||||
{
|
||||
const double inv = unit > 1e-20f ? 1.0 / unit : 1.0;
|
||||
const double A[12] = {inv, 0, 0, -inv * center[0],
|
||||
0, inv, 0, -inv * center[1],
|
||||
0, 0, inv, -inv * center[2]};
|
||||
vc.centers = dsparse::scene_centers(nullptr, 0, c.means.data(),
|
||||
c.num, 3, A);
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(*_engine_mutex);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "app/gui/Ui.h"
|
||||
|
||||
#include "i18n/catalog/Gui.h"
|
||||
#include "i18n/catalog/TrainFields.h"
|
||||
#include "app/gui/GlLoader.h" // GL types + 1.1 entry points
|
||||
|
||||
#include "imgui.h"
|
||||
@@ -61,20 +62,44 @@ void fov_to_intrinsics(float fov_deg, int w, int h, const char* model,
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void ViewportPanel::reset_pose(float radius) {
|
||||
// Match the web viewer's cam.reset() exactly: target = client-frame
|
||||
// origin (the normalized frame is centered on the CAMERA POSES via
|
||||
// center_method="poses", i.e. the captured object for object-centric
|
||||
// datasets -- NOT the point-cloud centroid, which distant background
|
||||
// points drag far away), pos = [0,0,1], then orbit(0, -250).
|
||||
_cam.pos[0] = 0; _cam.pos[1] = 0; _cam.pos[2] = 1;
|
||||
// The web viewer's cam.reset() about the chosen centre: target = centre,
|
||||
// pos = centre + [0,0,1], then orbit(0, -250).
|
||||
float c[3];
|
||||
center_shared(c);
|
||||
_cam.pos[0] = c[0]; _cam.pos[1] = c[1]; _cam.pos[2] = c[2] + 1.0f;
|
||||
_cam.rot[0] = _cam.rot[1] = _cam.rot[2] = 0; _cam.rot[3] = 1;
|
||||
_cam.target[0] = _cam.target[1] = _cam.target[2] = 0;
|
||||
_cam.target[0] = c[0]; _cam.target[1] = c[1]; _cam.target[2] = c[2];
|
||||
_cam.orbit(0, -250);
|
||||
_home = _cam;
|
||||
_home_dist = radius;
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
void ViewportPanel::set_centers(const dsparse::CenterTable* centers, bool has_cameras) {
|
||||
_centers_known = centers != nullptr;
|
||||
_center_has_cameras = has_cameras;
|
||||
if (centers) _centers = *centers;
|
||||
}
|
||||
|
||||
int ViewportPanel::effective_center_mode() const {
|
||||
using M = dsparse::CenterMode;
|
||||
if (_center_has_cameras) return _center_mode;
|
||||
switch ((M)_center_mode) {
|
||||
case M::CameraMedian: return (int)M::PointMedian;
|
||||
case M::CameraMean:
|
||||
case M::CameraFocus: return (int)M::PointMean;
|
||||
default: return _center_mode;
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportPanel::center_shared(float out[3]) const {
|
||||
if (!_centers_known) {
|
||||
out[0] = out[1] = out[2] = 0.0f;
|
||||
return;
|
||||
}
|
||||
shared_point(_centers[_center_mode].data(), out);
|
||||
}
|
||||
|
||||
void ViewportPanel::compute_framing(const spirula::TrainerSession& session) {
|
||||
reset_pose(1.0f);
|
||||
|
||||
@@ -105,6 +130,8 @@ bool ViewportPanel::maybe_frame(const spirula::TrainerSession& session) {
|
||||
std::to_string(session.ds.points.num());
|
||||
if (key == _framed_key) return false; // same dataset: keep the pose
|
||||
_framed_key = key;
|
||||
const dsparse::CenterTable centers = dsparse::scene_centers(session.ds);
|
||||
set_centers(¢ers, session.ds.num_cameras > 0);
|
||||
compute_framing(session);
|
||||
_show_cams = true; // default on for a fresh dataset preview
|
||||
return true;
|
||||
@@ -350,6 +377,10 @@ void ViewportPanel::attach_preview_data(const ParsedDataset& ds,
|
||||
_last_error = "preview renderer unavailable (OpenGL 3.2 required)";
|
||||
return;
|
||||
}
|
||||
{
|
||||
const dsparse::CenterTable centers = dsparse::scene_centers(ds);
|
||||
set_centers(¢ers, ds.num_cameras > 0);
|
||||
}
|
||||
if (first) {
|
||||
_framed_key = key;
|
||||
reset_pose(radius);
|
||||
@@ -391,6 +422,14 @@ void ViewportPanel::attach_preview_mesh(const meshing::MeshData& mesh,
|
||||
_last_error = "preview renderer unavailable (OpenGL 3.2 required)";
|
||||
return;
|
||||
}
|
||||
{
|
||||
double A[12];
|
||||
for (int i = 0; i < 12; i++) A[i] = to_normalized ? to_normalized[i] : (i % 5 == 0);
|
||||
const dsparse::CenterTable centers = dsparse::scene_centers(
|
||||
nullptr, 0, mesh.V.empty() ? nullptr : mesh.V[0].data(),
|
||||
(int64_t)mesh.V.size(), 3, A);
|
||||
set_centers(¢ers, false);
|
||||
}
|
||||
if (key != _framed_key) {
|
||||
_framed_key = key;
|
||||
reset_pose(radius);
|
||||
@@ -420,6 +459,7 @@ void ViewportPanel::attach_scene(const ViewerRenderConfig& cfg,
|
||||
_buffer_idx = std::min<int>(_buffer_idx, (int)_buffer_keys.size() - 1);
|
||||
_has_cameras = false;
|
||||
_show_cams = false;
|
||||
set_centers(&cfg.centers, cfg.center_cameras);
|
||||
if (key != _framed_key) {
|
||||
_framed_key = key;
|
||||
reset_pose(radius);
|
||||
@@ -670,6 +710,41 @@ void ViewportPanel::draw_controls(bool engine) {
|
||||
}
|
||||
ui::help_on_hover(msg::viewport_level_cameras_help);
|
||||
}
|
||||
if (_centers_known) {
|
||||
namespace fld = spirula::i18n::msg::field;
|
||||
auto center_label = [](int mode) -> const char* {
|
||||
const spirula::i18n::Msg* m =
|
||||
fld::choice_label("scene_center", dsparse::kCenterModeNames[mode]);
|
||||
return m ? m->get() : dsparse::kCenterModeNames[mode];
|
||||
};
|
||||
auto is_camera_mode = [](int mode) {
|
||||
using M = dsparse::CenterMode;
|
||||
return mode == (int)M::CameraMedian || mode == (int)M::CameraMean ||
|
||||
mode == (int)M::CameraFocus;
|
||||
};
|
||||
place(px(170.0f) + st.ItemInnerSpacing.x + text_w(msg::viewport_center.get()));
|
||||
ImGui::SetNextItemWidth(px(170.0f));
|
||||
if (ui::BeginComboRaw(ui::detail::label(msg::viewport_center),
|
||||
center_label(effective_center_mode()))) {
|
||||
for (int i = 0; i < dsparse::kNumCenterModes; i++) {
|
||||
if (!_center_has_cameras && is_camera_mode(i)) continue;
|
||||
if (ui::SelectableRaw(center_label(i), i == _center_mode) &&
|
||||
i != _center_mode) {
|
||||
_center_mode = i;
|
||||
// The model stays where it is; the pivot moves to the new
|
||||
// centre, and so does the pose Reset view returns to.
|
||||
float c[3];
|
||||
center_shared(c);
|
||||
recenter_at(c);
|
||||
const NavCamera live = _cam;
|
||||
reset_pose(_home_dist);
|
||||
_cam = live;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
ui::help_on_hover(msg::viewport_center_help);
|
||||
}
|
||||
if (engine) {
|
||||
place(px(66.0f) + st.ItemInnerSpacing.x +
|
||||
text_w(msg::viewport_scale.get()));
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include "app/webviewer/RenderWorker.h"
|
||||
#include "core/ColorSpace.h"
|
||||
#include "data/DatasetParser.h"
|
||||
#include "app/gui/NavCamera.h"
|
||||
#include "app/gui/PreviewRenderer.h"
|
||||
|
||||
@@ -64,12 +65,9 @@ public:
|
||||
const std::string& key, float radius = 1.0f);
|
||||
// Engine renderer (needs engine_ready).
|
||||
void attach(spirula::TrainerSession& session);
|
||||
// Engine renderer over something that is not a training session -- a splat
|
||||
// file opened in the viewer (SplatViewer). `key` identifies the scene, so
|
||||
// reopening the same file keeps the pose; `radius` is the scene radius in
|
||||
// the client's normalized frame, which is 1 for anything normalized.
|
||||
// There are no training cameras behind this, so the frustum controls are
|
||||
// not offered.
|
||||
// Engine renderer over a file (SplatViewer): `key` keeps the pose across
|
||||
// a reopen and `radius` is the scene radius in the client frame; the
|
||||
// centering menu comes from cfg.centers.
|
||||
void attach_scene(const ViewerRenderConfig& cfg, const ViewerHooks& hooks,
|
||||
const std::string& key, float radius = 1.0f);
|
||||
// The render-option controls a VIEWER gets, on top of attach_scene. The
|
||||
@@ -132,8 +130,17 @@ private:
|
||||
enum class Mode { None, Preview, Engine };
|
||||
|
||||
void compute_framing(const spirula::TrainerSession& session);
|
||||
// The client-frame default pose (web viewer cam.reset() + orbit(0,-250)).
|
||||
// The client-frame default pose (web viewer cam.reset() + orbit(0,-250)),
|
||||
// about the chosen centre.
|
||||
void reset_pose(float radius);
|
||||
// The centering choices, in the model frame. `has_cameras` says whether
|
||||
// the camera statistics are real or fell back to the point ones.
|
||||
void set_centers(const dsparse::CenterTable* centers, bool has_cameras);
|
||||
// The chosen centre in the shared frame; the origin when none is known.
|
||||
void center_shared(float out[3]) const;
|
||||
// The mode the menu shows: the point statistic a camera one fell back to
|
||||
// when there are no cameras.
|
||||
int effective_center_mode() const;
|
||||
// Frame the scene only when a different dataset arrives; a preview ->
|
||||
// engine transition on the same dataset keeps the navigated pose and
|
||||
// intrinsics (no jump when training starts).
|
||||
@@ -215,6 +222,12 @@ private:
|
||||
bool _align_identity = true;
|
||||
bool _level_cameras = true;
|
||||
bool _gauge_metric = false;
|
||||
// What the view orbits about and Reset view frames (dsparse::CenterMode),
|
||||
// a point per mode in the model frame. Moves only the camera.
|
||||
int _center_mode = (int)dsparse::CenterMode::CameraMedian;
|
||||
dsparse::CenterTable _centers{};
|
||||
bool _centers_known = false;
|
||||
bool _center_has_cameras = false;
|
||||
// Model units per unit of the navigated frame: what turns the grid's cell
|
||||
// size into a length (ParsedDataset::train_frame_scale).
|
||||
float _scene_scale = 1.0f;
|
||||
|
||||
@@ -50,6 +50,11 @@ struct ViewerRenderConfig {
|
||||
// Which engine scene slot this worker renders (Engine.h "Viewer scenes").
|
||||
// < 0 = whatever is bound, which is what a training session wants.
|
||||
int scene_slot = -1;
|
||||
// What the client's view orbits about and Reset frames, one point per
|
||||
// dsparse::CenterMode in the client's normalized frame. `center_cameras`
|
||||
// is false over a file, which has none, so those modes are not offered.
|
||||
dsparse::CenterTable centers{};
|
||||
bool center_cameras = false;
|
||||
};
|
||||
|
||||
struct ViewerHooks {
|
||||
|
||||
@@ -38,6 +38,8 @@ struct ViewerServer::Impl {
|
||||
HttpServer http;
|
||||
RenderWorker worker;
|
||||
std::vector<std::string> buffer_keys;
|
||||
dsparse::CenterTable centers{};
|
||||
bool center_cameras = false;
|
||||
bool started = false;
|
||||
|
||||
// ---- endpoint handlers -------------------------------------------------
|
||||
@@ -169,6 +171,24 @@ struct ViewerServer::Impl {
|
||||
body += "]";
|
||||
return HttpResponse::json(body);
|
||||
}
|
||||
|
||||
// What the client cannot work out for itself: it holds no splats and no
|
||||
// camera table, only the normalized frame both are expressed in.
|
||||
HttpResponse handle_scene() {
|
||||
std::string body = "{\"center_modes\": [";
|
||||
for (int i = 0; i < dsparse::kNumCenterModes; i++) {
|
||||
char one[128];
|
||||
std::snprintf(one, sizeof one,
|
||||
"%s{\"name\": \"%s\", \"center\": [%.9g, %.9g, %.9g]}",
|
||||
i ? ", " : "", dsparse::kCenterModeNames[i],
|
||||
centers[i][0], centers[i][1], centers[i][2]);
|
||||
body += one;
|
||||
}
|
||||
body += "], \"has_cameras\": ";
|
||||
body += center_cameras ? "true" : "false";
|
||||
body += "}";
|
||||
return HttpResponse::json(body);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -191,6 +211,8 @@ void ViewerServer::start(const std::string& host, int port,
|
||||
cfg.base_camera_size = viewer_upload_cameras(post);
|
||||
viewer_upload_grid(post);
|
||||
|
||||
im.centers = cfg.centers;
|
||||
im.center_cameras = cfg.center_cameras;
|
||||
im.worker.start(std::move(cfg), std::move(hooks));
|
||||
im.buffer_keys = im.worker.buffer_keys();
|
||||
im.started = true;
|
||||
@@ -200,6 +222,7 @@ void ViewerServer::start(const std::string& host, int port,
|
||||
im.http.route("/render", [&im](const HttpRequest& r) { return im.handle_render(r); });
|
||||
im.http.route("/pick", [&im](const HttpRequest& r) { return im.handle_pick(r); });
|
||||
im.http.route("/buffers", [&im](const HttpRequest&) { return im.handle_buffers(); });
|
||||
im.http.route("/scene", [&im](const HttpRequest&) { return im.handle_scene(); });
|
||||
im.http.route("/progress", [&im](const HttpRequest&) {
|
||||
return HttpResponse::json(im.hooks.progress_json ? im.hooks.progress_json()
|
||||
: "{}");
|
||||
|
||||
@@ -443,6 +443,10 @@
|
||||
</div>
|
||||
<input type="range" id="move-speed" min="-2" max="2" step="0.1" value="0" autocomplete="off"/>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label"><span>Center</span></div>
|
||||
<select id="center-mode" autocomplete="off" title="What the view orbits about and Reset Camera frames. Moves the view, never the model."></select>
|
||||
</div>
|
||||
<button class="btn" id="btn-reset-pose">Reset Camera</button>
|
||||
</div>
|
||||
|
||||
@@ -637,9 +641,10 @@ const cam = {
|
||||
forward() { return quat.rotVec(this.rot, [0,0,-1]); },
|
||||
|
||||
reset() {
|
||||
this.pos = [0, 0, 1];
|
||||
const c = Scene.center();
|
||||
this.pos = [c[0], c[1], c[2] + 1];
|
||||
this.rot = quat.identity();
|
||||
this.target = [0, 0, 0];
|
||||
this.target = c.slice();
|
||||
Nav.orbit(0, -250);
|
||||
},
|
||||
};
|
||||
@@ -910,6 +915,63 @@ function showHint(text) {
|
||||
// HTTP client
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
// The centering choices, which only the server can compute: the client holds
|
||||
// no splats and no camera table, just the normalized frame both live in.
|
||||
const Scene = {
|
||||
modes: [], // [{name, center}] in dsparse::CenterMode order
|
||||
hasCameras: false,
|
||||
mode: 2, // camera position median
|
||||
center() {
|
||||
const m = this.modes[this.mode];
|
||||
return m ? m.center.slice() : [0, 0, 0];
|
||||
},
|
||||
// A file has no cameras, so a camera statistic falls back to the point one.
|
||||
isCameraMode(i) { return i === 2 || i === 3 || i === 5; },
|
||||
fallback(i) { return i === 2 ? 1 : 4; },
|
||||
};
|
||||
|
||||
const CENTER_LABELS = {
|
||||
"none": "None (model origin)",
|
||||
"point-median": "Point cloud median",
|
||||
"camera-median": "Camera position median",
|
||||
"camera-focus": "Camera focus",
|
||||
"point-mean": "Point cloud mean",
|
||||
"camera-mean": "Camera position mean",
|
||||
};
|
||||
|
||||
async function fetchScene() {
|
||||
try {
|
||||
const response = await fetch(`${CFG.serverUrl}/scene`);
|
||||
if (!response.ok) throw new Error("no /scene");
|
||||
const j = await response.json();
|
||||
Scene.modes = j.center_modes || [];
|
||||
Scene.hasCameras = !!j.has_cameras;
|
||||
} catch (e) {
|
||||
// A server without /scene (an older build) keeps the model origin.
|
||||
Scene.modes = [];
|
||||
Scene.hasCameras = false;
|
||||
}
|
||||
const sel = $("center-mode");
|
||||
sel.innerHTML = "";
|
||||
if (!Scene.modes.length) {
|
||||
sel.parentElement.parentElement.style.display = "none";
|
||||
return;
|
||||
}
|
||||
if (!Scene.hasCameras && Scene.isCameraMode(Scene.mode))
|
||||
Scene.mode = Scene.fallback(Scene.mode);
|
||||
Scene.modes.forEach((m, i) => {
|
||||
if (!Scene.hasCameras && Scene.isCameraMode(i)) return;
|
||||
const o = document.createElement("option");
|
||||
o.value = String(i);
|
||||
o.textContent = CENTER_LABELS[m.name] || m.name;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
sel.value = String(Scene.mode);
|
||||
// The pose was reset before the centres arrived; put it where it belongs.
|
||||
cam.reset();
|
||||
Viewer.requestRender();
|
||||
}
|
||||
|
||||
async function fetchBuffers() {
|
||||
try {
|
||||
const response = await fetch(`${CFG.serverUrl}/buffers`);
|
||||
@@ -1377,6 +1439,13 @@ $("btn-reset-pose").addEventListener("click", () => {
|
||||
Viewer.requestRender();
|
||||
});
|
||||
|
||||
// The model stays where it is: the orbit pivot moves to the new centre, and so
|
||||
// does the pose Reset Camera returns to.
|
||||
$("center-mode").addEventListener("change", e => {
|
||||
Scene.mode = +e.target.value;
|
||||
recenterAt(Scene.center());
|
||||
});
|
||||
|
||||
$("btn-pause").addEventListener("click", async () => {
|
||||
try {
|
||||
const response = await fetch('/pause-toggle');
|
||||
@@ -1460,6 +1529,7 @@ setInterval(updateProgress, 1000);
|
||||
cam.reset();
|
||||
showHint(hintFor("turntable"));
|
||||
fetchBuffers();
|
||||
fetchScene();
|
||||
Viewer.loopRender();
|
||||
// Initial res label
|
||||
const res = getResolution($("res-slider").value);
|
||||
|
||||
@@ -138,6 +138,7 @@ inline int train_tier_rank(const char* tier) {
|
||||
X(std::string, center_method, "poses", "scene", "expert", "poses|focus|none|gsplat") \
|
||||
X(bool, auto_scale_poses, true, "scene", "expert", "") \
|
||||
X(float, outlier_threshold, kTrainInf, "scene", "basic", "") \
|
||||
X(std::string, scene_center, "none", "scene", "basic", "none|point-median|camera-median|camera-focus|point-mean|camera-mean") \
|
||||
X(std::optional<float>, relative_scale, std::nullopt, "scene", "expert", "") \
|
||||
X(std::string, train_frame, "points", "scene", "expert", "normalized|camera|points") \
|
||||
\
|
||||
@@ -347,7 +348,7 @@ struct TrainConfig {
|
||||
X(depth_dir) X(normal_dir) X(metashape_xml) X(metashape_ply) \
|
||||
X(metashape_psx) X(train_resolution_divisor) X(downscale_rounding_mode) \
|
||||
X(orientation_method) X(center_method) X(auto_scale_poses) \
|
||||
X(outlier_threshold) X(train_frame) X(eval_mode) X(train_split_fraction) \
|
||||
X(outlier_threshold) X(scene_center) X(train_frame) X(eval_mode) X(train_split_fraction) \
|
||||
X(eval_interval) X(depth_unit_scale_factor) X(validation_fraction) \
|
||||
X(warp_to_pinhole) X(warp_spherical_to_pinhole) X(warp_face_fit) \
|
||||
X(warp_back_face) \
|
||||
|
||||
+24
-10
@@ -11,6 +11,8 @@
|
||||
// that to the POST-split arrays engine_setup_data_manager consumes: identity
|
||||
// at K=1, or the pinhole faces camhost::plan_split_faces cuts a wide camera into.
|
||||
|
||||
#include "data/SceneCenter.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
@@ -59,8 +61,10 @@ struct ColmapImage {
|
||||
std::string name; // path relative to the image dir
|
||||
};
|
||||
|
||||
// xyz stays double: a geo-referenced model puts the cloud millions of units
|
||||
// from the origin, where float has a resolution of a metre.
|
||||
struct ColmapPoints3D {
|
||||
std::vector<float> xyz; // [N, 3] flat
|
||||
std::vector<double> xyz; // [N, 3] flat
|
||||
std::vector<uint8_t> rgb; // [N, 3] flat
|
||||
int64_t num() const { return (int64_t)xyz.size() / 3; }
|
||||
};
|
||||
@@ -138,6 +142,11 @@ struct DatasetParserConfig {
|
||||
// the geometric median of all camera positions. inf = off (default).
|
||||
float outlier_threshold = std::numeric_limits<float>::infinity();
|
||||
|
||||
// Which point of the raw frame becomes the training frame's origin: a
|
||||
// dsparse::CenterMode name. Computed over ALL post-outlier frames and every
|
||||
// seed point, in double, before anything is narrowed to float.
|
||||
std::string center_mode = "none";
|
||||
|
||||
// Pixel size of an image file (data/ImageProbe.h). Set: every camera trains
|
||||
// at its own image's resolution. Null: a caller with no decoders -- the
|
||||
// WebAssembly viewer, given a dataset's cameras but never its pixels.
|
||||
@@ -205,6 +214,12 @@ struct ParsedDataset {
|
||||
// Seed point cloud in the training frame.
|
||||
ColmapPoints3D points;
|
||||
|
||||
// p_train = p_raw - center, where p_raw is the frame the files came in
|
||||
// (COLMAP's own, or nerfstudio's with applied_transform undone). Zero
|
||||
// unless DatasetParserConfig::center_mode asked for one.
|
||||
std::array<double, 3> center{0.0, 0.0, 0.0};
|
||||
std::string center_mode = "none";
|
||||
|
||||
// 1 / scale_factor of the would-be normalized frame. Computed over ALL
|
||||
// frames, before the eval_mode subset is dropped.
|
||||
float train_frame_scale = 1.0f;
|
||||
@@ -317,20 +332,19 @@ PostSplitCameras bake_post_split(const ParsedDataset& ds,
|
||||
// ===========================================================================
|
||||
namespace dsparse {
|
||||
|
||||
// Normalized-frame scale factor over c2w [N,3,4] (orient="up",
|
||||
// center="poses", auto-scale). Only the scalar matters for
|
||||
// train_frame="points". Returns 1/max_abs.
|
||||
double compute_normalized_scale_factor(const std::vector<float>& c2w, int64_t n);
|
||||
|
||||
// Writes T_n_from_camera = scale * [R_align | -R_align @ center] (row-major
|
||||
// 4x4) and returns scale_factor; the viewer remap is inv(that @ applied).
|
||||
// `R_out` takes R_align alone, the one part of it a viewer can offer to skip.
|
||||
double compute_normalized_transform(const std::vector<float>& c2w, int64_t n,
|
||||
// T_n_from_camera = scale * [R_align | -R_align @ center] (row-major 4x4)
|
||||
// over c2w [N,3,4], orient="up" / center="poses"; returns scale_factor. The
|
||||
// viewer remap is inv(that @ applied); `R_out` is R_align alone.
|
||||
double compute_normalized_transform(const double* c2w, int64_t n,
|
||||
double T_out[16], double R_out[9] = nullptr);
|
||||
|
||||
// inv([A|b; 0 1]) for a general invertible 3x3 A (row-major 4x4 in/out).
|
||||
void invert_affine4x4(const double in[16], double out[16]);
|
||||
|
||||
// Every centering mode over a parsed dataset, in its NORMALIZED frame --
|
||||
// which is what both viewers navigate.
|
||||
CenterTable scene_centers(const ParsedDataset& ds);
|
||||
|
||||
// eval_mode subset over N sorted frames, honouring cfg.split; identity for
|
||||
// "all". `names` are image filenames (used by eval_mode="filename").
|
||||
std::vector<int64_t> train_subset(int64_t n, const std::vector<std::string>& names,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
// SceneCenter -- where a scene's origin goes: the `--scene-center` choices
|
||||
// and the viewports' centering menu, over cameras and points in one frame.
|
||||
// Implemented in parsers/DatasetCommon.cpp; kept apart from DatasetParser.h
|
||||
// so the WebGL viewer's fast-math source can call it without the parsers.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace dsparse {
|
||||
|
||||
// Where a scene's origin goes. The names are the `--scene-center` choices and
|
||||
// the viewport's centering menu, in that order.
|
||||
enum class CenterMode {
|
||||
None = 0, PointMedian, CameraMedian, CameraFocus, PointMean, CameraMean
|
||||
};
|
||||
constexpr const char* kCenterModeNames[] = {
|
||||
"none", "point-median", "camera-median", "camera-focus", "point-mean",
|
||||
"camera-mean"};
|
||||
constexpr int kNumCenterModes = 6;
|
||||
// Throws on a name not in kCenterModeNames; "" is `none`.
|
||||
CenterMode center_mode_from_name(const std::string& name);
|
||||
|
||||
// Weiszfeld from the per-axis median, ~8 passes over `pos`: n points of
|
||||
// `stride` elements, the first three being xyz. `max_samples` > 0 strides
|
||||
// over at most that many of them; the zero vector when n == 0.
|
||||
std::array<double, 3> geometric_median(const double* pos, int64_t n,
|
||||
int stride = 3, int64_t max_samples = 0);
|
||||
std::array<double, 3> geometric_median(const float* pos, int64_t n,
|
||||
int stride = 3, int64_t max_samples = 0);
|
||||
|
||||
// The point the cameras look at: the least-squares intersection of the
|
||||
// optical axes of every camera that has it in front, iterated from `init`
|
||||
// (camera_utils.focus_of_attention). c2w [N,3,4], OpenGL convention.
|
||||
std::array<double, 3> focus_of_attention(const double* c2w, int64_t n,
|
||||
const double init[3]);
|
||||
|
||||
// The center `mode` names over c2w [N,3,4] and m points of `stride` elements.
|
||||
// A point mode with no points falls back to the camera mode of the same
|
||||
// statistic and vice versa; over nothing at all it is the origin.
|
||||
std::array<double, 3> scene_center(CenterMode mode, const double* c2w, int64_t n,
|
||||
const double* points, int64_t m,
|
||||
int stride = 3, int64_t max_samples = 0);
|
||||
std::array<double, 3> scene_center(CenterMode mode, const double* c2w, int64_t n,
|
||||
const float* points, int64_t m,
|
||||
int stride = 3, int64_t max_samples = 0);
|
||||
|
||||
// One centre per mode, in mode order.
|
||||
using CenterTable = std::array<std::array<float, 3>, kNumCenterModes>;
|
||||
|
||||
// Every mode at once over c2w [N,3,4] (may be null) and m points of `stride`
|
||||
// values, each mapped through `to_model` (row-major 3x4 similarity, null =
|
||||
// identity). A viewport centre needs no more than 2^18 samples.
|
||||
CenterTable scene_centers(const double* c2w, int64_t n,
|
||||
const float* points, int64_t m,
|
||||
int stride = 3, const double* to_model = nullptr);
|
||||
CenterTable scene_centers(const double* c2w, int64_t n,
|
||||
const double* points, int64_t m,
|
||||
int stride = 3, const double* to_model = nullptr);
|
||||
|
||||
} // namespace dsparse
|
||||
@@ -0,0 +1,294 @@
|
||||
// SceneTransform.cpp -- see SceneTransform.h.
|
||||
|
||||
#include "data/SceneTransform.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace spirula {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr double kPi = 3.14159265358979323846;
|
||||
constexpr double kDeg = 180.0 / kPi;
|
||||
|
||||
// Right-handed rotation about axis `n` (0 = x, 1 = y, 2 = z), row-major.
|
||||
void axis_rotation(int n, double angle, double R[9]) {
|
||||
const double c = std::cos(angle), s = std::sin(angle);
|
||||
const int i = (n + 1) % 3, j = (n + 2) % 3;
|
||||
std::fill(R, R + 9, 0.0);
|
||||
R[n*3 + n] = 1.0;
|
||||
R[i*3 + i] = c; R[i*3 + j] = -s;
|
||||
R[j*3 + i] = s; R[j*3 + j] = c;
|
||||
}
|
||||
|
||||
void mat3_mul(const double A[9], const double B[9], double out[9]) {
|
||||
double t[9];
|
||||
for (int r = 0; r < 3; r++)
|
||||
for (int c = 0; c < 3; c++)
|
||||
t[r*3 + c] = A[r*3]*B[c] + A[r*3+1]*B[3+c] + A[r*3+2]*B[6+c];
|
||||
std::copy(t, t + 9, out);
|
||||
}
|
||||
|
||||
int axis_index(char c) {
|
||||
switch (c) {
|
||||
case 'x': case 'X': return 0;
|
||||
case 'y': case 'Y': return 1;
|
||||
case 'z': case 'Z': return 2;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ---- JSON text ------------------------------------------------------------
|
||||
|
||||
std::string num(double v) {
|
||||
if (v == 0.0) v = 0.0; // no "-0"
|
||||
char buf[40];
|
||||
std::snprintf(buf, sizeof buf, "%.17g", v);
|
||||
return buf;
|
||||
}
|
||||
|
||||
std::string arr(const double* v, int n) {
|
||||
std::string s = "[";
|
||||
for (int i = 0; i < n; i++) s += (i ? ", " : "") + num(v[i]);
|
||||
return s + "]";
|
||||
}
|
||||
|
||||
// Rows of a row-major matrix as nested arrays.
|
||||
std::string rows(const double* m, int nrow, int ncol) {
|
||||
std::string s = "[";
|
||||
for (int r = 0; r < nrow; r++)
|
||||
s += (r ? ", " : "") + arr(m + r * ncol, ncol);
|
||||
return s + "]";
|
||||
}
|
||||
|
||||
std::string transposed(const double* m, int nrow, int ncol) {
|
||||
std::string s = "[";
|
||||
for (int c = 0; c < ncol; c++)
|
||||
for (int r = 0; r < nrow; r++)
|
||||
s += ((r || c) ? ", " : "") + num(m[r*ncol + c]);
|
||||
return s + "]";
|
||||
}
|
||||
|
||||
std::string quoted(const std::string& s) {
|
||||
std::string out = "\"";
|
||||
for (char c : s) {
|
||||
if (c == '"' || c == '\\') out += '\\';
|
||||
out += c;
|
||||
}
|
||||
return out + "\"";
|
||||
}
|
||||
|
||||
std::string euler_block(const double R[9], const char* pad) {
|
||||
static const char* kOrders[] = {"xyz", "xzy", "yxz", "yzx", "zxy", "zyx"};
|
||||
std::string s;
|
||||
auto entry = [&](const char* name, const double a[3], bool last) {
|
||||
double deg[3] = {a[0]*kDeg, a[1]*kDeg, a[2]*kDeg};
|
||||
s += std::string(pad) + " \"" + name + "\": {\"radians\": " + arr(a, 3) +
|
||||
", \"degrees\": " + arr(deg, 3) + "}" + (last ? "\n" : ",\n");
|
||||
};
|
||||
s += std::string(pad) + "\"intrinsic\": {\n";
|
||||
for (int i = 0; i < 6; i++) {
|
||||
double a[3];
|
||||
rotation_to_euler_intrinsic(R, kOrders[i], a);
|
||||
entry(kOrders[i], a, i == 5);
|
||||
}
|
||||
s += std::string(pad) + "},\n";
|
||||
s += std::string(pad) + "\"extrinsic\": {\n";
|
||||
for (int i = 0; i < 6; i++) {
|
||||
// Fixed-axis abc is body-axis cba with the angles read backwards.
|
||||
char rev[3] = {kOrders[i][2], kOrders[i][1], kOrders[i][0]};
|
||||
double a[3], b[3];
|
||||
rotation_to_euler_intrinsic(R, rev, a);
|
||||
b[0] = a[2]; b[1] = a[1]; b[2] = a[0];
|
||||
entry(kOrders[i], b, i == 5);
|
||||
}
|
||||
s += std::string(pad) + "}\n";
|
||||
return s;
|
||||
}
|
||||
|
||||
std::string similarity_block(const SceneTransform& T, const char* pad) {
|
||||
const std::string p(pad);
|
||||
double q[4];
|
||||
rotation_to_quaternion(T.R, q);
|
||||
const double qxyzw[4] = {q[1], q[2], q[3], q[0]};
|
||||
|
||||
// Axis-angle from the quaternion; the axis of a zero rotation is +Z by
|
||||
// convention (any unit vector would do).
|
||||
double angle = 2.0 * std::atan2(std::sqrt(q[1]*q[1] + q[2]*q[2] + q[3]*q[3]), q[0]);
|
||||
double axis[3] = {0, 0, 1};
|
||||
const double sn = std::sqrt(q[1]*q[1] + q[2]*q[2] + q[3]*q[3]);
|
||||
if (sn > 1e-15) for (int i = 0; i < 3; i++) axis[i] = q[1 + i] / sn;
|
||||
const double rotvec[3] = {axis[0]*angle, axis[1]*angle, axis[2]*angle};
|
||||
|
||||
// p_out = s R (p + t_pre) <=> t = s R t_pre.
|
||||
double t_pre[3];
|
||||
for (int r = 0; r < 3; r++)
|
||||
t_pre[r] = (T.R[r]*T.t[0] + T.R[3+r]*T.t[1] + T.R[6+r]*T.t[2]) / T.scale;
|
||||
|
||||
double M[16] = {0};
|
||||
for (int r = 0; r < 3; r++) {
|
||||
for (int c = 0; c < 3; c++) M[r*4 + c] = T.scale * T.R[r*3 + c];
|
||||
M[r*4 + 3] = T.t[r];
|
||||
}
|
||||
M[15] = 1.0;
|
||||
|
||||
std::string s;
|
||||
s += p + "\"scale\": " + num(T.scale) + ",\n";
|
||||
s += p + "\"translation\": " + arr(T.t, 3) + ",\n";
|
||||
s += p + "\"translation_before_rotation\": " + arr(t_pre, 3) + ",\n";
|
||||
s += p + "\"rotation\": {\n";
|
||||
s += p + " \"matrix_3x3\": " + rows(T.R, 3, 3) + ",\n";
|
||||
s += p + " \"matrix_3x3_flat_row_major\": " + arr(T.R, 9) + ",\n";
|
||||
s += p + " \"matrix_3x3_flat_column_major\": " + transposed(T.R, 3, 3) + ",\n";
|
||||
s += p + " \"quaternion_wxyz\": " + arr(q, 4) + ",\n";
|
||||
s += p + " \"quaternion_xyzw\": " + arr(qxyzw, 4) + ",\n";
|
||||
s += p + " \"axis_angle\": {\"axis\": " + arr(axis, 3) + ", \"angle_rad\": " +
|
||||
num(angle) + ", \"angle_deg\": " + num(angle * kDeg) + "},\n";
|
||||
s += p + " \"rotation_vector\": " + arr(rotvec, 3) + ",\n";
|
||||
s += p + " \"euler\": {\n";
|
||||
s += euler_block(T.R, (p + " ").c_str());
|
||||
s += p + " }\n";
|
||||
s += p + "},\n";
|
||||
s += p + "\"matrix_4x4\": " + rows(M, 4, 4) + ",\n";
|
||||
s += p + "\"matrix_4x4_flat_row_major\": " + arr(M, 16) + ",\n";
|
||||
s += p + "\"matrix_4x4_flat_column_major\": " + transposed(M, 4, 4) + ",\n";
|
||||
s += p + "\"matrix_3x4_flat_row_major\": " + arr(M, 12) + "\n";
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
SceneTransform SceneTransform::inverse() const {
|
||||
SceneTransform inv;
|
||||
for (int r = 0; r < 3; r++)
|
||||
for (int c = 0; c < 3; c++) inv.R[r*3 + c] = R[c*3 + r];
|
||||
inv.scale = 1.0 / scale;
|
||||
for (int r = 0; r < 3; r++)
|
||||
inv.t[r] = -(inv.R[r*3]*t[0] + inv.R[r*3+1]*t[1] + inv.R[r*3+2]*t[2]) * inv.scale;
|
||||
return inv;
|
||||
}
|
||||
|
||||
// Shepperd's method: divide by the largest of the four candidates, which is
|
||||
// what keeps the result exact for every rotation angle including 180 degrees.
|
||||
void rotation_to_quaternion(const double R[9], double q[4]) {
|
||||
const double tr = R[0] + R[4] + R[8];
|
||||
double w, x, y, z;
|
||||
if (tr > 0.0) {
|
||||
const double s = std::sqrt(tr + 1.0) * 2.0;
|
||||
w = 0.25 * s;
|
||||
x = (R[7] - R[5]) / s;
|
||||
y = (R[2] - R[6]) / s;
|
||||
z = (R[3] - R[1]) / s;
|
||||
} else if (R[0] > R[4] && R[0] > R[8]) {
|
||||
const double s = std::sqrt(1.0 + R[0] - R[4] - R[8]) * 2.0;
|
||||
w = (R[7] - R[5]) / s;
|
||||
x = 0.25 * s;
|
||||
y = (R[1] + R[3]) / s;
|
||||
z = (R[2] + R[6]) / s;
|
||||
} else if (R[4] > R[8]) {
|
||||
const double s = std::sqrt(1.0 + R[4] - R[0] - R[8]) * 2.0;
|
||||
w = (R[2] - R[6]) / s;
|
||||
x = (R[1] + R[3]) / s;
|
||||
y = 0.25 * s;
|
||||
z = (R[5] + R[7]) / s;
|
||||
} else {
|
||||
const double s = std::sqrt(1.0 + R[8] - R[0] - R[4]) * 2.0;
|
||||
w = (R[3] - R[1]) / s;
|
||||
x = (R[2] + R[6]) / s;
|
||||
y = (R[5] + R[7]) / s;
|
||||
z = 0.25 * s;
|
||||
}
|
||||
const double n = std::sqrt(w*w + x*x + y*y + z*z);
|
||||
q[0] = w / n; q[1] = x / n; q[2] = y / n; q[3] = z / n;
|
||||
if (q[0] < 0.0) for (int i = 0; i < 4; i++) q[i] = -q[i];
|
||||
}
|
||||
|
||||
void euler_intrinsic_to_rotation(const char order[3], const double a[3],
|
||||
double R[9]) {
|
||||
double acc[9] = {1, 0, 0, 0, 1, 0, 0, 0, 1}, step[9];
|
||||
for (int k = 0; k < 3; k++) {
|
||||
axis_rotation(axis_index(order[k]), a[k], step);
|
||||
mat3_mul(acc, step, acc);
|
||||
}
|
||||
std::copy(acc, acc + 9, R);
|
||||
}
|
||||
|
||||
// For R = R_i(a) R_j(b) R_k(c): R[i][k] = e sin b with e = +1 when ijk is a
|
||||
// cyclic permutation, and the other two angles fall out of the row and column
|
||||
// through that entry. At |sin b| = 1 only a +- c is determined; c is set to 0.
|
||||
void rotation_to_euler_intrinsic(const double R[9], const char order[3],
|
||||
double out[3]) {
|
||||
const int i = axis_index(order[0]), j = axis_index(order[1]),
|
||||
k = axis_index(order[2]);
|
||||
const double e = ((j == (i + 1) % 3) ? 1.0 : -1.0);
|
||||
auto at = [&](int r, int c) { return R[r*3 + c]; };
|
||||
const double sb = std::clamp(e * at(i, k), -1.0, 1.0);
|
||||
out[1] = std::asin(sb);
|
||||
if (std::fabs(sb) < 1.0 - 1e-12) {
|
||||
out[0] = std::atan2(-e * at(j, k), at(k, k));
|
||||
out[2] = std::atan2(-e * at(i, j), at(i, i));
|
||||
} else {
|
||||
out[0] = std::atan2(sb * at(j, i), at(j, j));
|
||||
out[2] = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
std::string scene_transform_json(const SceneTransform& train_from_world,
|
||||
const std::string& center_mode,
|
||||
const double center_world[3]) {
|
||||
const SceneTransform world_from_train = train_from_world.inverse();
|
||||
std::string s = "{\n";
|
||||
s += " \"format\": \"spirula-scene-transform\",\n";
|
||||
s += " \"version\": 1,\n";
|
||||
s += " \"readme\": [\n";
|
||||
const char* lines[] = {
|
||||
"p_train = train_from_world.scale * train_from_world.rotation.matrix_3x3 "
|
||||
"@ p_world + train_from_world.translation, for a column vector p.",
|
||||
"train_from_world is the ACTIVE transform taking a point of the "
|
||||
"dataset's own frame (world) into the frame the splats were trained in "
|
||||
"(train); read passively, it gives a fixed point's train coordinates "
|
||||
"from its world coordinates. world_from_train is its inverse.",
|
||||
"matrix_4x4 acts on column vectors [x, y, z, 1]. "
|
||||
"matrix_4x4_flat_row_major lists it row by row; "
|
||||
"matrix_4x4_flat_column_major column by column, which is also the "
|
||||
"row-major layout of its transpose, the matrix that acts on row "
|
||||
"vectors [x, y, z, 1] @ M^T.",
|
||||
"rotation.matrix_3x3 acts on column vectors. quaternion_wxyz is "
|
||||
"(w, x, y, z) and quaternion_xyzw is (x, y, z, w), the same unit "
|
||||
"quaternion. axis_angle is a unit axis with a right-handed angle; "
|
||||
"rotation_vector is axis * angle_rad.",
|
||||
"euler.intrinsic.abc = [a1, a2, a3] means R = R_a(a1) @ R_b(a2) @ "
|
||||
"R_c(a3): rotate about the body axis a, then the new b, then the new "
|
||||
"c. euler.extrinsic.abc = [a1, a2, a3] means R = R_c(a3) @ R_b(a2) @ "
|
||||
"R_a(a1): rotate about the fixed axis a, then b, then c. So "
|
||||
"extrinsic.xyz = [roll about X, pitch about Y, yaw about Z] applied in "
|
||||
"that order, and intrinsic.zyx = [yaw, pitch, roll]. Every entry is "
|
||||
"given in radians and in degrees.",
|
||||
"translation is applied AFTER the rotation and scale (p_out = s R p + "
|
||||
"t). translation_before_rotation is the equivalent shift applied first "
|
||||
"(p_out = s R (p + t_pre)); for train_from_world it is minus "
|
||||
"centering.center_world.",
|
||||
};
|
||||
const int n_lines = (int)(sizeof(lines) / sizeof(lines[0]));
|
||||
for (int i = 0; i < n_lines; i++)
|
||||
s += " " + quoted(lines[i]) + (i + 1 < n_lines ? ",\n" : "\n");
|
||||
s += " ],\n";
|
||||
s += " \"centering\": {\"mode\": " + quoted(center_mode) +
|
||||
", \"center_world\": " + arr(center_world, 3) + "},\n";
|
||||
s += " \"train_from_world\": {\n";
|
||||
s += similarity_block(train_from_world, " ");
|
||||
s += " },\n";
|
||||
s += " \"world_from_train\": {\n";
|
||||
s += similarity_block(world_from_train, " ");
|
||||
s += " }\n";
|
||||
s += "}\n";
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace spirula
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
// SceneTransform -- the similarity between a dataset's own frame and the
|
||||
// frame the splats are trained in, and the scene_transform.json a run leaves
|
||||
// beside config.json: one transform spelled every common way (matrices,
|
||||
// quaternions, Euler angles, both directions) so nothing has to be converted
|
||||
// by hand downstream.
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace spirula {
|
||||
|
||||
// p_train = scale * R @ p_world + t. R row-major.
|
||||
struct SceneTransform {
|
||||
double R[9] = {1, 0, 0, 0, 1, 0, 0, 0, 1};
|
||||
double t[3] = {0, 0, 0};
|
||||
double scale = 1.0;
|
||||
|
||||
// p_world = R^T (p_train - t) / scale.
|
||||
SceneTransform inverse() const;
|
||||
};
|
||||
|
||||
// Unit quaternion (w, x, y, z) of a row-major rotation matrix.
|
||||
void rotation_to_quaternion(const double R[9], double q[4]);
|
||||
|
||||
// Tait-Bryan angles (radians) of R = R_a(x) R_b(y) R_c(z) for an intrinsic
|
||||
// axis order such as "xyz": the first rotation is about the first letter.
|
||||
// Extrinsic "abc" is intrinsic "cba" with the angles reversed.
|
||||
void rotation_to_euler_intrinsic(const double R[9], const char order[3],
|
||||
double out[3]);
|
||||
void euler_intrinsic_to_rotation(const char order[3], const double a[3],
|
||||
double R[9]);
|
||||
|
||||
// The JSON text. `center_mode` and `center_world` say how the translation was
|
||||
// chosen (DatasetParser.h center_mode; the point of the dataset frame that
|
||||
// became the origin).
|
||||
std::string scene_transform_json(const SceneTransform& train_from_world,
|
||||
const std::string& center_mode,
|
||||
const double center_world[3]);
|
||||
|
||||
} // namespace spirula
|
||||
@@ -168,7 +168,7 @@ ColmapPoints3D read_points3D_binary(const std::string& recon_dir) {
|
||||
pts.rgb.reserve(n * 3);
|
||||
for (uint64_t i = 0; i < n; i++) {
|
||||
r.skip(sizeof(uint64_t)); // point3D_id
|
||||
for (int k = 0; k < 3; k++) pts.xyz.push_back((float)r.read<double>());
|
||||
for (int k = 0; k < 3; k++) pts.xyz.push_back(r.read<double>());
|
||||
for (int k = 0; k < 3; k++) pts.rgb.push_back(r.read<uint8_t>());
|
||||
r.skip(sizeof(double)); // reprojection error
|
||||
uint64_t track_len = r.read<uint64_t>();
|
||||
@@ -287,7 +287,7 @@ ColmapPoints3D read_points3D_text(const std::string& recon_dir) {
|
||||
while (r.next_line(&s, &e)) {
|
||||
char* p;
|
||||
fast_strtol(s, &p); // point3D_id
|
||||
for (int k = 0; k < 3; k++) pts.xyz.push_back((float)fast_strtod(p, &p));
|
||||
for (int k = 0; k < 3; k++) pts.xyz.push_back(fast_strtod(p, &p));
|
||||
for (int k = 0; k < 3; k++) pts.rgb.push_back((uint8_t)fast_strtol(p, &p));
|
||||
// reprojection error + track: rest of line, skipped
|
||||
}
|
||||
@@ -523,16 +523,16 @@ void qvec2rotmat(const std::array<double, 4>& q, double R[3][3]) {
|
||||
// COLMAP w2c -> nerfstudio/OpenGL c2w:
|
||||
// c2w[:3,:3] = R^T with columns 1, 2 negated (OpenCV -> OpenGL axis flip)
|
||||
// c2w[:3,3] = -R^T @ t
|
||||
void colmap_to_c2w(const ColmapImage& im, float* out12) {
|
||||
void colmap_to_c2w(const ColmapImage& im, double* out12) {
|
||||
double R[3][3];
|
||||
qvec2rotmat(im.qvec, R);
|
||||
static const double flip[3] = {1.0, -1.0, -1.0};
|
||||
for (int r = 0; r < 3; r++) {
|
||||
for (int c = 0; c < 3; c++)
|
||||
out12[r*4 + c] = (float)(R[c][r] * flip[c]);
|
||||
out12[r*4 + c] = R[c][r] * flip[c];
|
||||
double t = 0.0;
|
||||
for (int c = 0; c < 3; c++) t -= R[c][r] * im.tvec[c];
|
||||
out12[r*4 + 3] = (float)t;
|
||||
out12[r*4 + 3] = t;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -772,7 +772,7 @@ ParsedDataset parse_colmap_dataset(const std::string& dataset_dir,
|
||||
|
||||
// ---- All-frame c2w (needed for outlier filter + train_frame_scale) ----
|
||||
int64_t n_all = (int64_t)frames.size();
|
||||
std::vector<float> c2w_all(n_all * 12);
|
||||
std::vector<double> c2w_all(n_all * 12);
|
||||
std::vector<double> positions(n_all * 3);
|
||||
for (int64_t i = 0; i < n_all; i++) {
|
||||
colmap_to_c2w(*frames[i], &c2w_all[i*12]);
|
||||
@@ -784,7 +784,7 @@ ParsedDataset parse_colmap_dataset(const std::string& dataset_dir,
|
||||
std::vector<char> keep = dsparse::outlier_keep_mask(
|
||||
positions, n_all, cfg.outlier_threshold);
|
||||
std::vector<const ColmapImage*> kept;
|
||||
std::vector<float> kept_c2w;
|
||||
std::vector<double> kept_c2w;
|
||||
for (int64_t i = 0; i < n_all; i++) {
|
||||
if (!keep[i]) continue;
|
||||
kept.push_back(frames[i]);
|
||||
@@ -795,13 +795,31 @@ ParsedDataset parse_colmap_dataset(const std::string& dataset_dir,
|
||||
n_all = (int64_t)frames.size();
|
||||
}
|
||||
|
||||
// In lenient (viewer) mode, tolerate a missing points3D file so a
|
||||
// cameras-only reconstruction still yields camera poses / frustums.
|
||||
ColmapPoints3D points;
|
||||
if (fmt.points3D == ColmapFmt::Bin)
|
||||
points = read_points3D_binary(recon_dir);
|
||||
else if (fmt.points3D == ColmapFmt::Text)
|
||||
points = read_points3D_text(recon_dir);
|
||||
|
||||
// ---- Centering, over ALL post-outlier frames and every point, while
|
||||
// both are still double --------------------------------------------------
|
||||
const dsparse::CenterMode center_mode = dsparse::center_mode_from_name(cfg.center_mode);
|
||||
const std::array<double, 3> center = dsparse::scene_center(
|
||||
center_mode, c2w_all.data(), n_all, points.xyz.data(), points.num());
|
||||
for (int64_t i = 0; i < n_all; i++)
|
||||
for (int r = 0; r < 3; r++) c2w_all[i*12 + r*4 + 3] -= center[r];
|
||||
for (int64_t i = 0; i < points.num(); i++)
|
||||
for (int r = 0; r < 3; r++) points.xyz[i*3 + r] -= center[r];
|
||||
|
||||
// ---- train_frame_scale + viewer remap transform over ALL post-outlier
|
||||
// frames (train + eval, matching the Python dataparser, which splits
|
||||
// after normalization). No applied_transform on the COLMAP path, so
|
||||
// train_to_normalized = inv(T_n_from_camera). -----------------------------
|
||||
double T_n[16], T_inv[16], R_align[9];
|
||||
double scale_factor =
|
||||
dsparse::compute_normalized_transform(c2w_all, n_all, T_n, R_align);
|
||||
dsparse::compute_normalized_transform(c2w_all.data(), n_all, T_n, R_align);
|
||||
dsparse::invert_affine4x4(T_n, T_inv);
|
||||
float train_frame_scale = (float)(scale_factor != 0.0 ? 1.0 / scale_factor : 1.0);
|
||||
|
||||
@@ -818,6 +836,9 @@ ParsedDataset parse_colmap_dataset(const std::string& dataset_dir,
|
||||
ds.train_frame_scale = train_frame_scale;
|
||||
for (int k = 0; k < 16; k++) ds.train_to_normalized[k] = (float)T_inv[k];
|
||||
for (int k = 0; k < 9; k++) ds.normalized_rotation[k] = (float)R_align[k];
|
||||
ds.center = center;
|
||||
ds.center_mode = dsparse::kCenterModeNames[(int)center_mode];
|
||||
ds.points = std::move(points);
|
||||
read_gauge(recon_dir, ds);
|
||||
ds.camera_models.reserve(N);
|
||||
ds.camera_distortions.reserve(N);
|
||||
@@ -882,7 +903,7 @@ ParsedDataset parse_colmap_dataset(const std::string& dataset_dir,
|
||||
ds.camera_distortions.push_back((int32_t)bi.distortion);
|
||||
if (any_redistort) ds.redistort[j] = bi.source;
|
||||
|
||||
std::copy(&c2w_all[i*12], &c2w_all[i*12] + 12, &ds.c2w[j*12]);
|
||||
for (int k = 0; k < 12; k++) ds.c2w[j*12 + k] = (float)c2w_all[i*12 + k];
|
||||
|
||||
// Auxiliary supervision buffers, discovered by filename convention.
|
||||
mask_files[j] = dsparse::find_aux_file(
|
||||
@@ -899,12 +920,6 @@ ParsedDataset parse_colmap_dataset(const std::string& dataset_dir,
|
||||
if (any_depth) ds.depth_filenames = std::move(depth_files);
|
||||
if (any_normal) ds.normal_filenames = std::move(normal_files);
|
||||
|
||||
// In lenient (viewer) mode, tolerate a missing points3D file so a
|
||||
// cameras-only reconstruction still yields camera poses / frustums.
|
||||
if (fmt.points3D == ColmapFmt::Bin)
|
||||
ds.points = read_points3D_binary(recon_dir);
|
||||
else if (fmt.points3D == ColmapFmt::Text)
|
||||
ds.points = read_points3D_text(recon_dir);
|
||||
// validation_fraction holds out part of the TRAIN set; the eval split is
|
||||
// already a held-out set, so it is all "train" from the DataManager's
|
||||
// point of view.
|
||||
|
||||
@@ -22,19 +22,11 @@ constexpr double kPi = 3.14159265358979323846; // MSVC has no M_PI by default
|
||||
namespace dsparse {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Normalized-frame scale:
|
||||
// up = normalize(mean of c2w Y columns)
|
||||
// R_align = rotation taking `up` to +Z (Rodrigues)
|
||||
// center = mean camera position
|
||||
// scale_factor = 1 / max |R_align @ (pos - center)|
|
||||
// TODO: "pca" / "vertical" / "gsplat" orientation_method and "focus" /
|
||||
// "gsplat" center_method. This implements the up/poses pair only;
|
||||
// check_config() warns when the config asks for anything else. The reference
|
||||
// implementation for the rest is kept in Python, on no code path:
|
||||
// reference/python/camera_utils.py plus the call-site algebra in
|
||||
// docs/notes/pose-normalization.md.
|
||||
// up = normalize(mean c2w Y column), R_align: up -> +Z, center = mean camera
|
||||
// position, scale = 1 / max |R_align (pos - center)|. The up/poses pair only;
|
||||
// the rest is reference/python/camera_utils.py and check_config() warns.
|
||||
// ---------------------------------------------------------------------------
|
||||
double compute_normalized_transform(const std::vector<float>& c2w, int64_t n,
|
||||
double compute_normalized_transform(const double* c2w, int64_t n,
|
||||
double T_out[16], double R_out[9]) {
|
||||
std::fill(T_out, T_out + 16, 0.0);
|
||||
T_out[0] = T_out[5] = T_out[10] = T_out[15] = 1.0;
|
||||
@@ -95,11 +87,6 @@ double compute_normalized_transform(const std::vector<float>& c2w, int64_t n,
|
||||
return scale_factor;
|
||||
}
|
||||
|
||||
double compute_normalized_scale_factor(const std::vector<float>& c2w, int64_t n) {
|
||||
double T[16];
|
||||
return compute_normalized_transform(c2w, n, T);
|
||||
}
|
||||
|
||||
void invert_affine4x4(const double in[16], double out[16]) {
|
||||
double a = in[0], b = in[1], c = in[2],
|
||||
d = in[4], e = in[5], g = in[6],
|
||||
@@ -233,10 +220,10 @@ std::string find_aux_file(const std::string& aux_dir_s, const std::string& rel_n
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Outlier rejection via geometric median
|
||||
// Scene centre + outlier rejection
|
||||
// ---------------------------------------------------------------------------
|
||||
namespace {
|
||||
double median_of(std::vector<double> v) {
|
||||
double median_of(std::vector<double>& v) {
|
||||
if (v.empty()) return 0.0;
|
||||
size_t mid = v.size() / 2;
|
||||
std::nth_element(v.begin(), v.begin() + mid, v.end());
|
||||
@@ -245,45 +232,78 @@ double median_of(std::vector<double> v) {
|
||||
double lo = *std::max_element(v.begin(), v.begin() + mid);
|
||||
return 0.5 * (lo + hi);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::vector<char> outlier_keep_mask(const std::vector<double>& pos,
|
||||
int64_t n, float threshold) {
|
||||
std::vector<char> keep(n, 1);
|
||||
if (!(threshold < std::numeric_limits<float>::infinity()) || n == 0)
|
||||
return keep;
|
||||
|
||||
// Geometric median via Weiszfeld with the zero-distance correction
|
||||
// (eps=0, maxiter=10).
|
||||
double y[3];
|
||||
for (int d = 0; d < 3; d++) {
|
||||
std::vector<double> col(n);
|
||||
for (int64_t i = 0; i < n; i++) col[i] = pos[i*3 + d];
|
||||
y[d] = median_of(col);
|
||||
template <typename T>
|
||||
std::array<double, 3> mean_of(const T* pos, int64_t n, int stride, int64_t step) {
|
||||
double sx = 0.0, sy = 0.0, sz = 0.0;
|
||||
int64_t cnt = 0;
|
||||
#pragma omp parallel for reduction(+:sx,sy,sz,cnt)
|
||||
for (int64_t i = 0; i < n; i += step) {
|
||||
sx += pos[i*stride]; sy += pos[i*stride+1]; sz += pos[i*stride+2];
|
||||
cnt++;
|
||||
}
|
||||
for (int it = 0; it < 10; it++) {
|
||||
double T[3] = {0, 0, 0}, Dinvs = 0.0;
|
||||
int64_t num_zeros = 0;
|
||||
for (int64_t i = 0; i < n; i++) {
|
||||
double dx = pos[i*3] - y[0], dy = pos[i*3+1] - y[1], dz = pos[i*3+2] - y[2];
|
||||
if (cnt <= 0) return {0.0, 0.0, 0.0};
|
||||
return {sx / (double)cnt, sy / (double)cnt, sz / (double)cnt};
|
||||
}
|
||||
|
||||
// Translation columns of c2w [N,3,4], packed [N,3].
|
||||
std::vector<double> camera_positions(const double* c2w, int64_t n) {
|
||||
std::vector<double> pos((size_t)std::max<int64_t>(n, 0) * 3);
|
||||
for (int64_t i = 0; i < n; i++)
|
||||
for (int r = 0; r < 3; r++) pos[i*3 + r] = c2w[i*12 + r*4 + 3];
|
||||
return pos;
|
||||
}
|
||||
|
||||
int64_t sample_step(int64_t n, int64_t max_samples) {
|
||||
if (max_samples <= 0 || n <= max_samples) return 1;
|
||||
return (n + max_samples - 1) / max_samples;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::array<double, 3> geometric_median_t(const T* pos, int64_t n, int stride,
|
||||
int64_t max_samples) {
|
||||
std::array<double, 3> y = {0.0, 0.0, 0.0};
|
||||
if (n <= 0) return y;
|
||||
const int64_t step = sample_step(n, max_samples);
|
||||
{
|
||||
std::vector<double> col;
|
||||
col.reserve((size_t)(n / step + 1));
|
||||
for (int d = 0; d < 3; d++) {
|
||||
col.clear();
|
||||
for (int64_t i = 0; i < n; i += step) col.push_back(pos[i*stride + d]);
|
||||
y[d] = median_of(col);
|
||||
}
|
||||
}
|
||||
// Weiszfeld with the zero-distance correction (Vardi & Zhang 2000), which
|
||||
// is what lets it start ON a sample. Eight passes converge to well under
|
||||
// 1e-6 of the spread on every capture tried; 20M points cost ~0.3 s.
|
||||
for (int it = 0; it < 8; it++) {
|
||||
double Tx = 0.0, Ty = 0.0, Tz = 0.0, Dinvs = 0.0;
|
||||
int64_t num_zeros = 0, cnt = 0;
|
||||
#pragma omp parallel for reduction(+:Tx,Ty,Tz,Dinvs,num_zeros,cnt)
|
||||
for (int64_t i = 0; i < n; i += step) {
|
||||
const T* p = pos + i*stride;
|
||||
double dx = p[0] - y[0], dy = p[1] - y[1], dz = p[2] - y[2];
|
||||
double D = std::sqrt(dx*dx + dy*dy + dz*dz);
|
||||
cnt++;
|
||||
if (D == 0.0) { num_zeros++; continue; }
|
||||
double w = 1.0 / D;
|
||||
Dinvs += w;
|
||||
for (int d = 0; d < 3; d++) T[d] += w * pos[i*3 + d];
|
||||
Tx += w * p[0]; Ty += w * p[1]; Tz += w * p[2];
|
||||
}
|
||||
if (num_zeros == cnt) break;
|
||||
double W[3] = {Tx, Ty, Tz};
|
||||
if (Dinvs > 0) for (int d = 0; d < 3; d++) W[d] /= Dinvs;
|
||||
double y1[3];
|
||||
if (num_zeros == n) break;
|
||||
if (Dinvs > 0) for (int d = 0; d < 3; d++) T[d] /= Dinvs;
|
||||
if (num_zeros == 0) {
|
||||
for (int d = 0; d < 3; d++) y1[d] = T[d];
|
||||
for (int d = 0; d < 3; d++) y1[d] = W[d];
|
||||
} else {
|
||||
double R[3], r = 0.0;
|
||||
for (int d = 0; d < 3; d++) { R[d] = (T[d] - y[d]) * Dinvs; r += R[d]*R[d]; }
|
||||
for (int d = 0; d < 3; d++) { R[d] = (W[d] - y[d]) * Dinvs; r += R[d]*R[d]; }
|
||||
r = std::sqrt(r);
|
||||
double rinv = (r == 0.0) ? 0.0 : (double)num_zeros / r;
|
||||
double a = std::max(0.0, 1.0 - rinv), b = std::min(1.0, rinv);
|
||||
for (int d = 0; d < 3; d++) y1[d] = a * T[d] + b * y[d];
|
||||
for (int d = 0; d < 3; d++) y1[d] = a * W[d] + b * y[d];
|
||||
}
|
||||
double diff = 0.0;
|
||||
for (int d = 0; d < 3; d++) {
|
||||
@@ -292,7 +312,170 @@ std::vector<char> outlier_keep_mask(const std::vector<double>& pos,
|
||||
}
|
||||
if (diff == 0.0) break;
|
||||
}
|
||||
return y;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::array<double, 3> geometric_median(const double* pos, int64_t n, int stride,
|
||||
int64_t max_samples) {
|
||||
return geometric_median_t(pos, n, stride, max_samples);
|
||||
}
|
||||
std::array<double, 3> geometric_median(const float* pos, int64_t n, int stride,
|
||||
int64_t max_samples) {
|
||||
return geometric_median_t(pos, n, stride, max_samples);
|
||||
}
|
||||
|
||||
std::array<double, 3> focus_of_attention(const double* c2w, int64_t n,
|
||||
const double init[3]) {
|
||||
std::array<double, 3> focus = {init[0], init[1], init[2]};
|
||||
if (n <= 0) return focus;
|
||||
// Optical axis is -Z of the OpenGL camera frame.
|
||||
std::vector<double> dir(n * 3), org = camera_positions(c2w, n);
|
||||
for (int64_t i = 0; i < n; i++)
|
||||
for (int r = 0; r < 3; r++) dir[i*3 + r] = -c2w[i*12 + r*4 + 2];
|
||||
std::vector<char> active(n);
|
||||
auto in_front = [&](int64_t i) {
|
||||
double s = 0.0;
|
||||
for (int r = 0; r < 3; r++) s += dir[i*3 + r] * (focus[r] - org[i*3 + r]);
|
||||
return s > 0.0;
|
||||
};
|
||||
int64_t num_active = 0;
|
||||
for (int64_t i = 0; i < n; i++) num_active += (active[i] = in_front(i));
|
||||
// Cameras only ever leave the active set, so this terminates.
|
||||
while (num_active > 1) {
|
||||
// Least squares over (I - d d^T) p = (I - d d^T) o for the active
|
||||
// rays; (I - d d^T) is symmetric idempotent, so M^T M = M.
|
||||
double A[3][3] = {}, b[3] = {};
|
||||
for (int64_t i = 0; i < n; i++) {
|
||||
if (!active[i]) continue;
|
||||
const double* d = &dir[i*3];
|
||||
const double* o = &org[i*3];
|
||||
for (int r = 0; r < 3; r++) {
|
||||
for (int c = 0; c < 3; c++) {
|
||||
double m = (r == c ? 1.0 : 0.0) - d[r] * d[c];
|
||||
A[r][c] += m;
|
||||
b[r] += m * o[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
double Ain[16] = {A[0][0], A[0][1], A[0][2], 0,
|
||||
A[1][0], A[1][1], A[1][2], 0,
|
||||
A[2][0], A[2][1], A[2][2], 0,
|
||||
0, 0, 0, 1};
|
||||
double Ainv[16];
|
||||
try { invert_affine4x4(Ain, Ainv); } catch (const std::exception&) { break; }
|
||||
for (int r = 0; r < 3; r++)
|
||||
focus[r] = Ainv[r*4]*b[0] + Ainv[r*4+1]*b[1] + Ainv[r*4+2]*b[2];
|
||||
int64_t still = 0;
|
||||
for (int64_t i = 0; i < n; i++) {
|
||||
if (!active[i]) continue;
|
||||
active[i] = in_front(i);
|
||||
still += active[i];
|
||||
}
|
||||
if (still == num_active) break;
|
||||
num_active = still;
|
||||
}
|
||||
return focus;
|
||||
}
|
||||
|
||||
CenterMode center_mode_from_name(const std::string& name) {
|
||||
if (name.empty()) return CenterMode::None; // the CLI's spelling of `none`
|
||||
for (int i = 0; i < kNumCenterModes; i++)
|
||||
if (name == kCenterModeNames[i]) return (CenterMode)i;
|
||||
throw std::runtime_error("unknown scene center mode '" + name + "'");
|
||||
}
|
||||
|
||||
namespace {
|
||||
template <typename T>
|
||||
std::array<double, 3> scene_center_t(CenterMode mode, const double* c2w, int64_t n,
|
||||
const T* points, int64_t m, int stride,
|
||||
int64_t max_samples) {
|
||||
if (m <= 0) {
|
||||
if (mode == CenterMode::PointMedian) mode = CenterMode::CameraMedian;
|
||||
if (mode == CenterMode::PointMean) mode = CenterMode::CameraMean;
|
||||
}
|
||||
if (n <= 0) {
|
||||
if (mode == CenterMode::CameraMedian) mode = CenterMode::PointMedian;
|
||||
if (mode == CenterMode::CameraMean || mode == CenterMode::CameraFocus)
|
||||
mode = CenterMode::PointMean;
|
||||
if (m <= 0) mode = CenterMode::None;
|
||||
}
|
||||
switch (mode) {
|
||||
case CenterMode::None: return {0.0, 0.0, 0.0};
|
||||
case CenterMode::PointMedian: return geometric_median_t(points, m, stride, max_samples);
|
||||
case CenterMode::PointMean: return mean_of(points, m, stride, sample_step(m, max_samples));
|
||||
case CenterMode::CameraMedian: {
|
||||
std::vector<double> pos = camera_positions(c2w, n);
|
||||
return geometric_median_t(pos.data(), n, 3, 0);
|
||||
}
|
||||
case CenterMode::CameraMean: {
|
||||
std::vector<double> pos = camera_positions(c2w, n);
|
||||
return mean_of(pos.data(), n, 3, 1);
|
||||
}
|
||||
case CenterMode::CameraFocus: {
|
||||
std::vector<double> pos = camera_positions(c2w, n);
|
||||
std::array<double, 3> mean = mean_of(pos.data(), n, 3, 1);
|
||||
return focus_of_attention(c2w, n, mean.data());
|
||||
}
|
||||
}
|
||||
return {0.0, 0.0, 0.0};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::array<double, 3> scene_center(CenterMode mode, const double* c2w, int64_t n,
|
||||
const double* points, int64_t m,
|
||||
int stride, int64_t max_samples) {
|
||||
return scene_center_t(mode, c2w, n, points, m, stride, max_samples);
|
||||
}
|
||||
std::array<double, 3> scene_center(CenterMode mode, const double* c2w, int64_t n,
|
||||
const float* points, int64_t m,
|
||||
int stride, int64_t max_samples) {
|
||||
return scene_center_t(mode, c2w, n, points, m, stride, max_samples);
|
||||
}
|
||||
|
||||
namespace {
|
||||
template <typename T>
|
||||
CenterTable scene_centers_t(const double* c2w, int64_t n, const T* points,
|
||||
int64_t m, int stride, const double* A) {
|
||||
CenterTable out{};
|
||||
for (int i = 0; i < kNumCenterModes; i++) {
|
||||
const std::array<double, 3> c = scene_center_t(
|
||||
(CenterMode)i, c2w, n, points, m, stride, 1 << 18);
|
||||
for (int r = 0; r < 3; r++)
|
||||
out[i][r] = A ? (float)(A[r*4]*c[0] + A[r*4+1]*c[1] + A[r*4+2]*c[2] + A[r*4+3])
|
||||
: (float)c[r];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CenterTable scene_centers(const double* c2w, int64_t n, const float* points,
|
||||
int64_t m, int stride, const double* to_model) {
|
||||
return scene_centers_t(c2w, n, points, m, stride, to_model);
|
||||
}
|
||||
CenterTable scene_centers(const double* c2w, int64_t n, const double* points,
|
||||
int64_t m, int stride, const double* to_model) {
|
||||
return scene_centers_t(c2w, n, points, m, stride, to_model);
|
||||
}
|
||||
|
||||
CenterTable scene_centers(const ParsedDataset& ds) {
|
||||
const int64_t n = std::min<int64_t>(ds.num_cameras, (int64_t)ds.c2w.size() / 12);
|
||||
std::vector<double> c2w(ds.c2w.begin(), ds.c2w.begin() + n * 12);
|
||||
double A[16] = {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};
|
||||
if (ds.train_frame_scale != 1.0f) {
|
||||
double T[16];
|
||||
for (int i = 0; i < 16; i++) T[i] = ds.train_to_normalized[i];
|
||||
invert_affine4x4(T, A);
|
||||
}
|
||||
return scene_centers_t(c2w.data(), n, ds.points.xyz.data(), ds.points.num(), 3, A);
|
||||
}
|
||||
|
||||
std::vector<char> outlier_keep_mask(const std::vector<double>& pos,
|
||||
int64_t n, float threshold) {
|
||||
std::vector<char> keep(n, 1);
|
||||
if (!(threshold < std::numeric_limits<float>::infinity()) || n == 0)
|
||||
return keep;
|
||||
std::array<double, 3> y = geometric_median(pos.data(), n, 3, 0);
|
||||
std::vector<double> dist(n);
|
||||
for (int64_t i = 0; i < n; i++) {
|
||||
double dx = pos[i*3] - y[0], dy = pos[i*3+1] - y[1], dz = pos[i*3+2] - y[2];
|
||||
|
||||
@@ -222,9 +222,9 @@ ColmapPoints3D read_ply_points(const std::string& path) {
|
||||
throw std::runtime_error("PLY: truncated " + path);
|
||||
for (int64_t i = 0; i < el.count; i++) {
|
||||
const uint8_t* row = (const uint8_t*)p + (size_t)i * stride;
|
||||
pts.xyz[i*3 + 0] = (float)ply_read_scalar(row + offsets[ix], el.props[ix].type);
|
||||
pts.xyz[i*3 + 1] = (float)ply_read_scalar(row + offsets[iy], el.props[iy].type);
|
||||
pts.xyz[i*3 + 2] = (float)ply_read_scalar(row + offsets[iz], el.props[iz].type);
|
||||
pts.xyz[i*3 + 0] = ply_read_scalar(row + offsets[ix], el.props[ix].type);
|
||||
pts.xyz[i*3 + 1] = ply_read_scalar(row + offsets[iy], el.props[iy].type);
|
||||
pts.xyz[i*3 + 2] = ply_read_scalar(row + offsets[iz], el.props[iz].type);
|
||||
pts.rgb[i*3 + 0] = to_u8(ply_read_scalar(row + offsets[ir], el.props[ir].type), el.props[ir].type);
|
||||
pts.rgb[i*3 + 1] = to_u8(ply_read_scalar(row + offsets[ig], el.props[ig].type), el.props[ig].type);
|
||||
pts.rgb[i*3 + 2] = to_u8(ply_read_scalar(row + offsets[ib], el.props[ib].type), el.props[ib].type);
|
||||
@@ -241,9 +241,9 @@ ColmapPoints3D read_ply_points(const std::string& path) {
|
||||
throw std::runtime_error("PLY: short ascii row in " + path);
|
||||
p = q;
|
||||
}
|
||||
pts.xyz[i*3 + 0] = (float)vals[ix];
|
||||
pts.xyz[i*3 + 1] = (float)vals[iy];
|
||||
pts.xyz[i*3 + 2] = (float)vals[iz];
|
||||
pts.xyz[i*3 + 0] = vals[ix];
|
||||
pts.xyz[i*3 + 1] = vals[iy];
|
||||
pts.xyz[i*3 + 2] = vals[iz];
|
||||
pts.rgb[i*3 + 0] = to_u8(vals[ir], el.props[ir].type);
|
||||
pts.rgb[i*3 + 1] = to_u8(vals[ig], el.props[ig].type);
|
||||
pts.rgb[i*3 + 2] = to_u8(vals[ib], el.props[ib].type);
|
||||
@@ -455,7 +455,7 @@ ParsedDataset parse_nerfstudio_meta(const JsonValue& meta,
|
||||
[](const Frame& a, const Frame& b) { return a.abs < b.abs; });
|
||||
|
||||
// ---- All-frame c2w -------------------------------------------------------
|
||||
auto read_c2w = [](const JsonValue& fr, float* out12) {
|
||||
auto read_c2w = [](const JsonValue& fr, double* out12) {
|
||||
const JsonValue* tm = fr.find("transform_matrix");
|
||||
if (!tm || !tm->is_array() || tm->arr.size() < 3)
|
||||
throw std::runtime_error("NerfstudioParser: bad transform_matrix");
|
||||
@@ -463,11 +463,11 @@ ParsedDataset parse_nerfstudio_meta(const JsonValue& meta,
|
||||
const JsonValue& row = tm->arr[r];
|
||||
if (!row.is_array() || row.arr.size() < 4)
|
||||
throw std::runtime_error("NerfstudioParser: bad transform_matrix row");
|
||||
for (int c = 0; c < 4; c++) out12[r*4 + c] = (float)row.arr[c].as_double();
|
||||
for (int c = 0; c < 4; c++) out12[r*4 + c] = row.arr[c].as_double();
|
||||
}
|
||||
};
|
||||
int64_t n_all = (int64_t)frames.size();
|
||||
std::vector<float> c2w_all(n_all * 12);
|
||||
std::vector<double> c2w_all(n_all * 12);
|
||||
std::vector<double> positions(n_all * 3);
|
||||
for (int64_t i = 0; i < n_all; i++) {
|
||||
read_c2w(*frames[i].j, &c2w_all[i*12]);
|
||||
@@ -479,7 +479,7 @@ ParsedDataset parse_nerfstudio_meta(const JsonValue& meta,
|
||||
std::vector<char> keep = dsparse::outlier_keep_mask(
|
||||
positions, n_all, cfg.outlier_threshold);
|
||||
std::vector<Frame> kept;
|
||||
std::vector<float> kept_c2w;
|
||||
std::vector<double> kept_c2w;
|
||||
for (int64_t i = 0; i < n_all; i++) {
|
||||
if (!keep[i]) continue;
|
||||
kept.push_back(frames[i]);
|
||||
@@ -490,11 +490,101 @@ ParsedDataset parse_nerfstudio_meta(const JsonValue& meta,
|
||||
n_all = (int64_t)frames.size();
|
||||
}
|
||||
|
||||
// ---- Seed points ------------------------------------------------------
|
||||
ColmapPoints3D points;
|
||||
{
|
||||
std::string ply_rel;
|
||||
if (const JsonValue* v = meta.find("ply_file_path")) ply_rel = v->as_string();
|
||||
else {
|
||||
for (const char* cand : {"sparse_pc.ply", "pointcloud.ply"})
|
||||
if (fs::exists(root / cand)) { ply_rel = cand; break; }
|
||||
}
|
||||
if (ply_rel.empty()) {
|
||||
// Lenient (viewer) mode: a transforms.json with no point cloud
|
||||
// still yields camera poses / frustums. The trainer requires the
|
||||
// seed cloud.
|
||||
if (cfg.require_image_files)
|
||||
throw std::runtime_error(
|
||||
"NerfstudioParser: no initial point cloud found (ply_file_path / "
|
||||
"sparse_pc.ply / pointcloud.ply)");
|
||||
} else if (cfg.require_image_files || fs::exists(root / ply_rel)) {
|
||||
points = read_ply_points((root / ply_rel).string());
|
||||
}
|
||||
}
|
||||
|
||||
// ---- applied_transform inverse (train_frame="points" branch): poses and
|
||||
// points go back to the ORIGINAL (pre-applied_transform) frame, which is
|
||||
// where the centre is taken. -------------------------------------------
|
||||
double A[3][3] = {{1,0,0},{0,1,0},{0,0,1}}, b[3] = {0, 0, 0};
|
||||
bool applied = false;
|
||||
if (const JsonValue* at = meta.find("applied_transform")) {
|
||||
for (int r = 0; r < 3; r++) {
|
||||
const JsonValue& row = at->arr.at(r);
|
||||
for (int c = 0; c < 3; c++) A[r][c] = row.arr.at(c).as_double();
|
||||
b[r] = row.arr.at(3).as_double();
|
||||
}
|
||||
for (int r = 0; r < 3 && !applied; r++)
|
||||
for (int c = 0; c < 3; c++)
|
||||
if (A[r][c] != (r == c ? 1.0 : 0.0) || b[r] != 0.0) { applied = true; break; }
|
||||
}
|
||||
std::vector<double> c2w_world = c2w_all;
|
||||
if (applied) {
|
||||
double Ai[3][3];
|
||||
invert3x3d(A, Ai);
|
||||
double bi[3];
|
||||
for (int r = 0; r < 3; r++)
|
||||
bi[r] = -(Ai[r][0]*b[0] + Ai[r][1]*b[1] + Ai[r][2]*b[2]);
|
||||
// c2w' = inv(T) @ c2w (c2w has implicit bottom row 0 0 0 1)
|
||||
for (int64_t i = 0; i < n_all; i++) {
|
||||
const double* m = &c2w_all[i*12];
|
||||
double* out = &c2w_world[i*12];
|
||||
for (int r = 0; r < 3; r++)
|
||||
for (int c = 0; c < 4; c++)
|
||||
out[r*4 + c] = Ai[r][0]*m[0*4+c] + Ai[r][1]*m[1*4+c]
|
||||
+ Ai[r][2]*m[2*4+c] + (c == 3 ? bi[r] : 0.0);
|
||||
}
|
||||
for (int64_t i = 0; i < points.num(); i++) {
|
||||
double* p = &points.xyz[i*3];
|
||||
double x = Ai[0][0]*p[0] + Ai[0][1]*p[1] + Ai[0][2]*p[2] + bi[0];
|
||||
double y = Ai[1][0]*p[0] + Ai[1][1]*p[1] + Ai[1][2]*p[2] + bi[1];
|
||||
double z = Ai[2][0]*p[0] + Ai[2][1]*p[1] + Ai[2][2]*p[2] + bi[2];
|
||||
p[0] = x; p[1] = y; p[2] = z;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Centering, over ALL post-outlier frames and every point, still in
|
||||
// double. The same shift is A @ center + b in the transforms.json frame,
|
||||
// after which the map between the two frames is A alone. ----------------
|
||||
const dsparse::CenterMode center_mode = dsparse::center_mode_from_name(cfg.center_mode);
|
||||
const std::array<double, 3> center = dsparse::scene_center(
|
||||
center_mode, c2w_world.data(), n_all, points.xyz.data(), points.num());
|
||||
double center_json[3];
|
||||
for (int r = 0; r < 3; r++)
|
||||
center_json[r] = A[r][0]*center[0] + A[r][1]*center[1] + A[r][2]*center[2] + b[r];
|
||||
for (int64_t i = 0; i < n_all; i++)
|
||||
for (int r = 0; r < 3; r++) {
|
||||
c2w_world[i*12 + r*4 + 3] -= center[r];
|
||||
c2w_all[i*12 + r*4 + 3] -= center_json[r];
|
||||
}
|
||||
for (int64_t i = 0; i < points.num(); i++)
|
||||
for (int r = 0; r < 3; r++) points.xyz[i*3 + r] -= center[r];
|
||||
|
||||
// ---- train_frame_scale + normalized-frame similarity (all post-outlier
|
||||
// frames, pre-split). ------------------------------------------------------
|
||||
// frames, pre-split), in the transforms.json frame so the levelling
|
||||
// rotation stays relative to the file's own axes. -----------------------
|
||||
double T_n_from_camera[16], R_align[9];
|
||||
double scale_factor = dsparse::compute_normalized_transform(
|
||||
c2w_all, n_all, T_n_from_camera, R_align);
|
||||
c2w_all.data(), n_all, T_n_from_camera, R_align);
|
||||
// train_to_normalized = inv(T_n_from_camera @ [A | 0])
|
||||
double T_n_from_train[16];
|
||||
for (int r = 0; r < 4; r++)
|
||||
for (int c = 0; c < 4; c++) {
|
||||
double v = 0.0;
|
||||
for (int m = 0; m < 3; m++)
|
||||
v += T_n_from_camera[r*4 + m] * (c < 3 ? A[m][c] : 0.0);
|
||||
if (c == 3) v += T_n_from_camera[r*4 + 3];
|
||||
T_n_from_train[r*4 + c] = v;
|
||||
}
|
||||
|
||||
// ---- eval_mode train subset ----------------------------------------------
|
||||
std::vector<std::string> names(n_all);
|
||||
@@ -505,6 +595,9 @@ ParsedDataset parse_nerfstudio_meta(const JsonValue& meta,
|
||||
const int64_t N = (int64_t)subset.size();
|
||||
ds.num_cameras = N;
|
||||
ds.train_frame_scale = (float)(scale_factor != 0.0 ? 1.0 / scale_factor : 1.0);
|
||||
ds.center = center;
|
||||
ds.center_mode = dsparse::kCenterModeNames[(int)center_mode];
|
||||
ds.points = std::move(points);
|
||||
ds.c2w.resize(N * 12);
|
||||
ds.intrins.resize(N * 4);
|
||||
ds.dist_coeffs.resize(N * kCameraDistortionParams);
|
||||
@@ -648,7 +741,7 @@ ParsedDataset parse_nerfstudio_meta(const JsonValue& meta,
|
||||
ds.intrins[j*4 + 1] = (float)fy;
|
||||
ds.intrins[j*4 + 2] = (float)cx;
|
||||
ds.intrins[j*4 + 3] = (float)cy;
|
||||
std::copy(&c2w_all[subset[j]*12], &c2w_all[subset[j]*12] + 12, &ds.c2w[j*12]);
|
||||
for (int k = 0; k < 12; k++) ds.c2w[j*12 + k] = (float)c2w_world[subset[j]*12 + k];
|
||||
|
||||
// Auxiliary buffers: explicit frame paths win; directory-convention
|
||||
// probing as fallback (_add_auxiliary_buffers). Unlike the Python
|
||||
@@ -679,80 +772,6 @@ ParsedDataset parse_nerfstudio_meta(const JsonValue& meta,
|
||||
if (any_normal) ds.normal_filenames = std::move(normal_files);
|
||||
print_lens_fits(lens_fits);
|
||||
|
||||
// ---- Seed points ------------------------------------------------------
|
||||
std::string ply_rel;
|
||||
if (const JsonValue* v = meta.find("ply_file_path")) ply_rel = v->as_string();
|
||||
else {
|
||||
for (const char* cand : {"sparse_pc.ply", "pointcloud.ply"})
|
||||
if (fs::exists(root / cand)) { ply_rel = cand; break; }
|
||||
}
|
||||
if (ply_rel.empty()) {
|
||||
// Lenient (viewer) mode: a transforms.json with no point cloud still
|
||||
// yields camera poses / frustums. The trainer requires the seed cloud.
|
||||
if (cfg.require_image_files)
|
||||
throw std::runtime_error(
|
||||
"NerfstudioParser: no initial point cloud found (ply_file_path / "
|
||||
"sparse_pc.ply / pointcloud.ply)");
|
||||
} else if (cfg.require_image_files || fs::exists(root / ply_rel)) {
|
||||
ds.points = read_ply_points((root / ply_rel).string());
|
||||
}
|
||||
|
||||
// ---- applied_transform inverse (train_frame="points" branch): poses and
|
||||
// points go back to the ORIGINAL (pre-applied_transform) frame. Also folds
|
||||
// into the viewer remap:
|
||||
// train_to_normalized = inv(T_n_from_camera @ applied)
|
||||
double T_n_from_train[16];
|
||||
std::copy(T_n_from_camera, T_n_from_camera + 16, T_n_from_train);
|
||||
if (const JsonValue* at = meta.find("applied_transform")) {
|
||||
double A[3][3], b[3];
|
||||
for (int r = 0; r < 3; r++) {
|
||||
const JsonValue& row = at->arr.at(r);
|
||||
for (int c = 0; c < 3; c++) A[r][c] = row.arr.at(c).as_double();
|
||||
b[r] = row.arr.at(3).as_double();
|
||||
}
|
||||
bool identity = true;
|
||||
for (int r = 0; r < 3 && identity; r++)
|
||||
for (int c = 0; c < 3; c++)
|
||||
if (A[r][c] != (r == c ? 1.0 : 0.0) || b[r] != 0.0) { identity = false; break; }
|
||||
if (!identity) {
|
||||
// T_n_from_train = T_n_from_camera @ applied (both affine, 0001 rows)
|
||||
double ap[16] = {A[0][0],A[0][1],A[0][2],b[0],
|
||||
A[1][0],A[1][1],A[1][2],b[1],
|
||||
A[2][0],A[2][1],A[2][2],b[2],
|
||||
0,0,0,1};
|
||||
for (int r = 0; r < 4; r++)
|
||||
for (int c = 0; c < 4; c++) {
|
||||
double v = 0.0;
|
||||
for (int m = 0; m < 4; m++)
|
||||
v += T_n_from_camera[r*4 + m] * ap[m*4 + c];
|
||||
T_n_from_train[r*4 + c] = v;
|
||||
}
|
||||
double Ai[3][3];
|
||||
invert3x3d(A, Ai);
|
||||
double bi[3];
|
||||
for (int r = 0; r < 3; r++)
|
||||
bi[r] = -(Ai[r][0]*b[0] + Ai[r][1]*b[1] + Ai[r][2]*b[2]);
|
||||
// c2w' = inv(T) @ c2w (c2w has implicit bottom row 0 0 0 1)
|
||||
for (int64_t j = 0; j < N; j++) {
|
||||
float* m = &ds.c2w[j*12];
|
||||
double out[3][4];
|
||||
for (int r = 0; r < 3; r++)
|
||||
for (int c = 0; c < 4; c++)
|
||||
out[r][c] = Ai[r][0]*m[0*4+c] + Ai[r][1]*m[1*4+c]
|
||||
+ Ai[r][2]*m[2*4+c] + (c == 3 ? bi[r] : 0.0);
|
||||
for (int r = 0; r < 3; r++)
|
||||
for (int c = 0; c < 4; c++) m[r*4+c] = (float)out[r][c];
|
||||
}
|
||||
for (int64_t i = 0; i < ds.points.num(); i++) {
|
||||
float* p = &ds.points.xyz[i*3];
|
||||
double x = Ai[0][0]*p[0] + Ai[0][1]*p[1] + Ai[0][2]*p[2] + bi[0];
|
||||
double y = Ai[1][0]*p[0] + Ai[1][1]*p[1] + Ai[1][2]*p[2] + bi[1];
|
||||
double z = Ai[2][0]*p[0] + Ai[2][1]*p[1] + Ai[2][2]*p[2] + bi[2];
|
||||
p[0] = (float)x; p[1] = (float)y; p[2] = (float)z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double T_remap[16];
|
||||
dsparse::invert_affine4x4(T_n_from_train, T_remap);
|
||||
for (int k = 0; k < 16; k++) ds.train_to_normalized[k] = (float)T_remap[k];
|
||||
|
||||
@@ -2281,6 +2281,51 @@ SS_MSG(viewport_level_cameras_help,
|
||||
"döndürür. Bu bir tahmindir ve eğik ya da ters 360 çekimlerde kötüdür; "
|
||||
"kapalıyken modelin kendi eksenleri kullanılır."));
|
||||
|
||||
SS_MSG(viewport_center,
|
||||
EN("center"), JA("中心"), ZH_HANS("中心"), ZH_HANT("中心"),
|
||||
KO("중심"), DE("Zentrum"), FR("centre"), ES("centro"),
|
||||
PT("centro"), IT("centro"), NL("centrum"), RU("центр"),
|
||||
TR("merkez"));
|
||||
|
||||
SS_MSG(viewport_center_help,
|
||||
EN("The point the view orbits about and Reset view frames: a statistic of "
|
||||
"the cameras or of the points, or the model's own origin. This moves "
|
||||
"only the view, never the model."),
|
||||
JA("視点が回転する中心であり、ビューのリセットで画面に収める点です。カメラ"
|
||||
"または点の統計値か、モデル自身の原点を選びます。動くのは視点だけで、モデル"
|
||||
"は動きません。"),
|
||||
ZH_HANS("视图绕其旋转、重置视图时对准的点:相机或点的某种统计量,或模型自身的"
|
||||
"原点。只移动视图,不移动模型。"),
|
||||
ZH_HANT("檢視繞其旋轉、重設檢視時對準的點:相機或點的某種統計量,或模型自身的"
|
||||
"原點。只移動檢視,不移動模型。"),
|
||||
KO("시점이 회전하는 중심이자 뷰 재설정이 맞추는 점입니다. 카메라나 점의 "
|
||||
"통계값, 또는 모델 자체의 원점 중에서 고릅니다. 시점만 움직이며 모델은 "
|
||||
"움직이지 않습니다."),
|
||||
DE("Der Punkt, um den die Ansicht kreist und den das Zurücksetzen einrahmt: "
|
||||
"eine Statistik der Kameras oder der Punkte, oder der Ursprung des "
|
||||
"Modells. Bewegt nur die Ansicht, nie das Modell."),
|
||||
FR("Le point autour duquel la vue tourne et que la réinitialisation cadre : "
|
||||
"une statistique des caméras ou des points, ou l'origine du modèle. Ne "
|
||||
"déplace que la vue, jamais le modèle."),
|
||||
ES("El punto alrededor del cual gira la vista y que el reinicio encuadra: "
|
||||
"una estadística de las cámaras o de los puntos, o el origen del modelo. "
|
||||
"Solo mueve la vista, nunca el modelo."),
|
||||
PT("O ponto em torno do qual a vista gira e que o reinício enquadra: uma "
|
||||
"estatística das câmeras ou dos pontos, ou a origem do modelo. Só move a "
|
||||
"vista, nunca o modelo."),
|
||||
IT("Il punto attorno a cui ruota la vista e che il ripristino inquadra: una "
|
||||
"statistica delle camere o dei punti, oppure l'origine del modello. "
|
||||
"Sposta solo la vista, mai il modello."),
|
||||
NL("Het punt waar het beeld omheen draait en dat beeld herstellen in beeld "
|
||||
"brengt: een statistiek van de camera's of de punten, of de oorsprong van "
|
||||
"het model. Verplaatst alleen het beeld, nooit het model."),
|
||||
RU("Точка, вокруг которой вращается вид и на которую его наводит сброс: "
|
||||
"статистика камер или точек либо начало координат модели. Двигает только "
|
||||
"вид, но не модель."),
|
||||
TR("Görünümün etrafında döndüğü ve görünüm sıfırlamanın çerçevelediği "
|
||||
"nokta: kameraların ya da noktaların bir istatistiği veya modelin kendi "
|
||||
"başlangıcı. Yalnızca görünümü oynatır, modeli asla."));
|
||||
|
||||
SS_MSG(viewport_scale,
|
||||
EN("resolution"), JA("解像度"), ZH_HANS("分辨率"), ZH_HANT("解析度"),
|
||||
KO("해상도"), DE("Auflösung"), FR("résolution"), ES("resolución"),
|
||||
|
||||
@@ -1076,6 +1076,23 @@ SS_MSG(parsed_dataset,
|
||||
TR("Kamera: {0} (bölmeden sonra {1}), başlangıç noktası: {2} "
|
||||
"(train_frame_scale={3})"));
|
||||
|
||||
// {1} is "x, y, z" in the dataset's frame; the shift lives on in
|
||||
// scene_transform.json, so this line is what a reader of the log sees first.
|
||||
SS_MSG(scene_centered,
|
||||
EN("Scene origin ({0}): {1} in the dataset's own frame"),
|
||||
JA("シーンの原点({0}): データセット自身の座標系で {1}"),
|
||||
ZH_HANS("场景原点({0}):数据集自身坐标系中的 {1}"),
|
||||
ZH_HANT("場景原點({0}):資料集自身座標系中的 {1}"),
|
||||
KO("장면 원점({0}): 데이터셋 자체 좌표계에서 {1}"),
|
||||
DE("Szenenursprung ({0}): {1} im eigenen Bezugssystem des Datensatzes"),
|
||||
FR("Origine de la scène ({0}) : {1} dans le repère propre du jeu de données"),
|
||||
ES("Origen de la escena ({0}): {1} en el sistema propio del conjunto de datos"),
|
||||
PT("Origem da cena ({0}): {1} no referencial próprio do conjunto de dados"),
|
||||
IT("Origine della scena ({0}): {1} nel sistema proprio del dataset"),
|
||||
NL("Oorsprong van de scène ({0}): {1} in het eigen stelsel van de dataset"),
|
||||
RU("Начало координат сцены ({0}): {1} в собственной системе набора данных"),
|
||||
TR("Sahne başlangıcı ({0}): veri kümesinin kendi çerçevesinde {1}"));
|
||||
|
||||
// Printed only when --input-depth-is-ray-depth was left unset and there are
|
||||
// depth maps to read; {0} is the convention the lens picked.
|
||||
SS_MSG(ray_depth_resolved,
|
||||
|
||||
@@ -2068,6 +2068,85 @@ SS_MSG(outlier_threshold_help,
|
||||
"daha çoğunu eler; birkaç yanlış kestirilmiş duruş sahneyi gerip ölçeğini "
|
||||
"bozduğunda işe yarar. Tüm kameraları tutmak için sonsuzda bırakın."));
|
||||
|
||||
SS_MSG(scene_center,
|
||||
EN("Scene centering"), JA("シーンの中心合わせ"), ZH_HANS("场景居中"),
|
||||
ZH_HANT("場景置中"), KO("장면 중심 맞추기"), DE("Szene zentrieren"),
|
||||
FR("Centrage de la scène"), ES("Centrado de la escena"),
|
||||
PT("Centralização da cena"), IT("Centratura della scena"),
|
||||
NL("Scène centreren"), RU("Центрирование сцены"), TR("Sahne ortalama"));
|
||||
SS_MSG(scene_center_help,
|
||||
EN("Move the dataset so this point becomes the origin before training, and "
|
||||
"record the shift in scene_transform.json in the output folder. Splats "
|
||||
"are then trained in the shifted frame. Useful for geo-referenced "
|
||||
"reconstructions whose coordinates are millions of units from the "
|
||||
"origin, where single precision would lose detail. `none` keeps the "
|
||||
"dataset's own frame."),
|
||||
JA("学習前にデータセットを移動し、この点を原点にします。移動量は出力フォルダの "
|
||||
"scene_transform.json に記録され、スプラットは移動後の座標系で学習されます。"
|
||||
"座標が原点から数百万単位も離れた地理参照付きの再構成では、単精度では細部が"
|
||||
"失われるため有効です。`none` はデータセット自身の座標系をそのまま使います。"),
|
||||
ZH_HANS("训练前平移数据集,使该点成为原点,并把平移量记录到输出文件夹的 "
|
||||
"scene_transform.json 中;泼溅在平移后的坐标系中训练。适用于坐标距原点"
|
||||
"数百万单位的地理参考重建,否则单精度会丢失细节。`none` 保留数据集自身"
|
||||
"的坐标系。"),
|
||||
ZH_HANT("訓練前平移資料集,使該點成為原點,並把平移量記錄到輸出資料夾的 "
|
||||
"scene_transform.json 中;潑濺在平移後的座標系中訓練。適用於座標距原點"
|
||||
"數百萬單位的地理參考重建,否則單精度會遺失細節。`none` 保留資料集自身"
|
||||
"的座標系。"),
|
||||
KO("학습 전에 데이터셋을 옮겨 이 점을 원점으로 삼고, 그 이동량을 출력 폴더의 "
|
||||
"scene_transform.json에 기록합니다. 스플랫은 옮겨진 좌표계에서 학습됩니다. "
|
||||
"좌표가 원점에서 수백만 단위 떨어진 지리 참조 복원에서는 단정밀도로 세부가 "
|
||||
"사라지므로 유용합니다. `none`은 데이터셋 자체의 좌표계를 그대로 둡니다."),
|
||||
DE("Verschiebt den Datensatz vor dem Training so, dass dieser Punkt zum "
|
||||
"Ursprung wird, und hält die Verschiebung in scene_transform.json im "
|
||||
"Ausgabeordner fest. Die Splats werden im verschobenen Bezugssystem "
|
||||
"trainiert. Nützlich für georeferenzierte Rekonstruktionen, deren "
|
||||
"Koordinaten Millionen Einheiten vom Ursprung entfernt liegen, wo einfache "
|
||||
"Genauigkeit Details verliert. `none` behält das Bezugssystem des "
|
||||
"Datensatzes."),
|
||||
FR("Déplace le jeu de données avant l'entraînement pour que ce point devienne "
|
||||
"l'origine, et note le décalage dans scene_transform.json dans le dossier "
|
||||
"de sortie. Les splats sont alors entraînés dans le repère décalé. Utile "
|
||||
"pour les reconstructions géoréférencées dont les coordonnées sont à des "
|
||||
"millions d'unités de l'origine, où la simple précision perd des détails. "
|
||||
"`none` garde le repère du jeu de données."),
|
||||
ES("Desplaza el conjunto de datos antes de entrenar para que este punto sea "
|
||||
"el origen, y anota el desplazamiento en scene_transform.json en la carpeta "
|
||||
"de salida. Los splats se entrenan entonces en el sistema desplazado. Útil "
|
||||
"para reconstrucciones georreferenciadas cuyas coordenadas están a millones "
|
||||
"de unidades del origen, donde la precisión simple pierde detalle. `none` "
|
||||
"conserva el sistema propio del conjunto de datos."),
|
||||
PT("Desloca o conjunto de dados antes do treino para que este ponto vire a "
|
||||
"origem, e registra o deslocamento em scene_transform.json na pasta de "
|
||||
"saída. Os splats são então treinados no referencial deslocado. Útil para "
|
||||
"reconstruções georreferenciadas cujas coordenadas ficam a milhões de "
|
||||
"unidades da origem, onde a precisão simples perde detalhe. `none` mantém "
|
||||
"o referencial do próprio conjunto de dados."),
|
||||
IT("Sposta il dataset prima dell'addestramento in modo che questo punto "
|
||||
"diventi l'origine, e annota lo spostamento in scene_transform.json nella "
|
||||
"cartella di output. Gli splat vengono quindi addestrati nel sistema "
|
||||
"spostato. Utile per ricostruzioni georeferenziate le cui coordinate "
|
||||
"distano milioni di unità dall'origine, dove la precisione singola perde "
|
||||
"dettaglio. `none` mantiene il sistema proprio del dataset."),
|
||||
NL("Verschuift de dataset vóór het trainen zodat dit punt de oorsprong wordt, "
|
||||
"en legt de verschuiving vast in scene_transform.json in de uitvoermap. De "
|
||||
"splats worden dan in het verschoven stelsel getraind. Handig voor "
|
||||
"gegeorefereerde reconstructies waarvan de coördinaten miljoenen eenheden "
|
||||
"van de oorsprong liggen, waar enkele precisie detail verliest. `none` "
|
||||
"behoudt het eigen stelsel van de dataset."),
|
||||
RU("Сдвигает набор данных перед обучением так, чтобы эта точка стала началом "
|
||||
"координат, и записывает сдвиг в scene_transform.json в папке вывода. "
|
||||
"Сплаты обучаются в сдвинутой системе координат. Полезно для "
|
||||
"геопривязанных реконструкций, координаты которых отстоят от начала на "
|
||||
"миллионы единиц, где одинарная точность теряет детали. `none` сохраняет "
|
||||
"собственную систему набора данных."),
|
||||
TR("Eğitimden önce veri kümesini bu nokta başlangıç olacak şekilde kaydırır "
|
||||
"ve kaydırmayı çıktı klasöründeki scene_transform.json dosyasına yazar. "
|
||||
"Splatlar kaydırılmış çerçevede eğitilir. Koordinatları başlangıçtan "
|
||||
"milyonlarca birim uzakta olan coğrafi referanslı yeniden kurmalar için "
|
||||
"yararlıdır; tek duyarlık orada ayrıntı kaybeder. `none` veri kümesinin "
|
||||
"kendi çerçevesini korur."));
|
||||
|
||||
SS_MSG(relative_scale,
|
||||
EN("Scene scale multiplier"), JA("シーンの倍率"),
|
||||
ZH_HANS("场景缩放倍数"), ZH_HANT("場景縮放倍數"), KO("장면 배율"),
|
||||
@@ -10099,6 +10178,53 @@ SS_MSG(choice_points,
|
||||
PT("pontos"), IT("punti"), NL("punten"), RU("точки"),
|
||||
TR("noktalar"));
|
||||
|
||||
SS_MSG(choice_none,
|
||||
EN("none"), JA("なし"), ZH_HANS("无"), ZH_HANT("無"),
|
||||
KO("없음"), DE("keine"), FR("aucun"), ES("ninguno"),
|
||||
PT("nenhum"), IT("nessuno"), NL("geen"), RU("нет"),
|
||||
TR("yok"));
|
||||
|
||||
SS_MSG(choice_point_median,
|
||||
EN("point cloud median"), JA("点群の中央値"), ZH_HANS("点云中位数"),
|
||||
ZH_HANT("點雲中位數"), KO("점군 중앙값"), DE("Median der Punktwolke"),
|
||||
FR("médiane du nuage de points"), ES("mediana de la nube de puntos"),
|
||||
PT("mediana da nuvem de pontos"), IT("mediana della nuvola di punti"),
|
||||
NL("mediaan van de puntenwolk"), RU("медиана облака точек"),
|
||||
TR("nokta bulutu ortancası"));
|
||||
|
||||
SS_MSG(choice_camera_median,
|
||||
EN("camera position median"), JA("カメラ位置の中央値"),
|
||||
ZH_HANS("相机位置中位数"), ZH_HANT("相機位置中位數"), KO("카메라 위치 중앙값"),
|
||||
DE("Median der Kamerapositionen"), FR("médiane des positions de caméra"),
|
||||
ES("mediana de las posiciones de cámara"),
|
||||
PT("mediana das posições das câmeras"),
|
||||
IT("mediana delle posizioni delle camere"),
|
||||
NL("mediaan van de cameraposities"), RU("медиана положений камер"),
|
||||
TR("kamera konumu ortancası"));
|
||||
|
||||
SS_MSG(choice_camera_focus,
|
||||
EN("camera focus"), JA("カメラの注視点"), ZH_HANS("相机注视点"),
|
||||
ZH_HANT("相機注視點"), KO("카메라 주시점"), DE("Kamerafokus"),
|
||||
FR("point de convergence des caméras"), ES("foco de las cámaras"),
|
||||
PT("foco das câmeras"), IT("fuoco delle camere"), NL("camerafocus"),
|
||||
RU("фокус камер"), TR("kamera odağı"));
|
||||
|
||||
SS_MSG(choice_point_mean,
|
||||
EN("point cloud mean"), JA("点群の平均"), ZH_HANS("点云平均"),
|
||||
ZH_HANT("點雲平均"), KO("점군 평균"), DE("Mittelwert der Punktwolke"),
|
||||
FR("moyenne du nuage de points"), ES("media de la nube de puntos"),
|
||||
PT("média da nuvem de pontos"), IT("media della nuvola di punti"),
|
||||
NL("gemiddelde van de puntenwolk"), RU("среднее облака точек"),
|
||||
TR("nokta bulutu ortalaması"));
|
||||
|
||||
SS_MSG(choice_camera_mean,
|
||||
EN("camera position mean"), JA("カメラ位置の平均"), ZH_HANS("相机位置平均"),
|
||||
ZH_HANT("相機位置平均"), KO("카메라 위치 평균"),
|
||||
DE("Mittelwert der Kamerapositionen"), FR("moyenne des positions de caméra"),
|
||||
ES("media de las posiciones de cámara"), PT("média das posições das câmeras"),
|
||||
IT("media delle posizioni delle camere"), NL("gemiddelde van de cameraposities"),
|
||||
RU("среднее положений камер"), TR("kamera konumu ortalaması"));
|
||||
|
||||
SS_MSG(choice_mean,
|
||||
EN("mean"), JA("平均"), ZH_HANS("平均"), ZH_HANT("平均"),
|
||||
KO("평균"), DE("Mittelwert"), FR("moyenne"), ES("media"),
|
||||
@@ -10196,6 +10322,13 @@ inline constexpr ChoiceText kChoiceText[] = {
|
||||
{"train_frame", "camera", &choice_camera},
|
||||
{"train_frame", "points", &choice_points},
|
||||
|
||||
{"scene_center", "none", &choice_none},
|
||||
{"scene_center", "point-median", &choice_point_median},
|
||||
{"scene_center", "camera-median", &choice_camera_median},
|
||||
{"scene_center", "camera-focus", &choice_camera_focus},
|
||||
{"scene_center", "point-mean", &choice_point_mean},
|
||||
{"scene_center", "camera-mean", &choice_camera_mean},
|
||||
|
||||
{"densify_score_mode", "mean", &choice_mean},
|
||||
{"densify_score_mode", "max", &choice_max},
|
||||
{"densify_score_mode", "median", &choice_median},
|
||||
|
||||
@@ -107,6 +107,14 @@ plain C++17 with no CUDA dependency (see `csrc/CameraModel.h`).
|
||||
optical axis). Splats use a one-pixel GPU depth pass; meshes use a WASM
|
||||
raycast; datasets pick the nearest point along the view ray. Works with every
|
||||
camera model.
|
||||
- The **Center** menu (Scene section, next to the up-axis toggle) picks what
|
||||
the view orbits about and fits to: the model's origin, the point cloud's
|
||||
geometric median or mean, or over a dataset the camera positions' median or
|
||||
mean or the point the cameras look at. Camera position median is the
|
||||
default; over a splat or mesh file, which has no cameras, the camera entries
|
||||
are disabled and the point statistics stand in. The same six modes are the
|
||||
trainer's `--scene-center` and the native GUI viewport's menu
|
||||
(`src/data/SceneCenter.h` is the one implementation).
|
||||
- One model at a time — dropping another replaces it and frees the previous GPU
|
||||
buffers. Replacing keeps the current viewpoint (the camera is only fitted for
|
||||
the first model; refresh the page to start over).
|
||||
|
||||
@@ -184,6 +184,17 @@
|
||||
<label class="toggle"><input autocomplete="off" type="checkbox" id="up-axis"><span class="track"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label"><span>Center</span></div>
|
||||
<select autocomplete="off" id="center-mode" title="What the view orbits about and fits to. Camera statistics need a dataset; over a splat or mesh file they fall back to the point ones.">
|
||||
<option value="0">None (model origin)</option>
|
||||
<option value="1">Point cloud median</option>
|
||||
<option value="2" selected>Camera position median</option>
|
||||
<option value="3">Camera focus</option>
|
||||
<option value="4">Point cloud mean</option>
|
||||
<option value="5">Camera position mean</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label"><span>Show Axes / Grid</span>
|
||||
<label class="toggle"><input autocomplete="off" type="checkbox" id="grid"><span class="track"></span></label>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
import {
|
||||
dsMount, dsEnumerate, dsParse, dsReadCameras, dsReadPoints,
|
||||
dsSummary, dsFitSphere, dsFrustumSize, dsLastError,
|
||||
dsSummary, dsFrustumSize, dsLastError,
|
||||
} from './wasm.js';
|
||||
|
||||
// Engine CameraModelType ints (ParsedDataset.camera_models / Common.cuh:130).
|
||||
@@ -319,9 +319,8 @@ export function parseDatasetComponent(token) {
|
||||
if (!ok) throw new Error(error || 'failed to parse dataset component');
|
||||
const cameras = dsReadCameras(); // copied
|
||||
const summary = dsSummary(); // copied
|
||||
const fit = dsFitSphere(); // copied
|
||||
const frustumSize = dsFrustumSize();
|
||||
const frustum = buildFrustums(cameras);
|
||||
const points = dsReadPoints(); // live heap views — upload before dsFree
|
||||
return { cameras, summary, fit, frustumSize, frustum, points, error };
|
||||
return { cameras, summary, frustumSize, frustum, points, error };
|
||||
}
|
||||
|
||||
+43
-8
@@ -1,7 +1,7 @@
|
||||
// Application entry: wires the WASM loader, WebGL renderer, camera controller,
|
||||
// and the UI panel together.
|
||||
|
||||
import { initWasm, loadModel, sortSplats, freeSplatSh, reduceSh, splatHistogram, meshHistogram, meshEdgeCount, fitSphere, raycastMesh, dsFree, dsPickPoint } from './wasm.js';
|
||||
import { initWasm, loadModel, sortSplats, freeSplatSh, reduceSh, splatHistogram, meshHistogram, meshEdgeCount, fitSphere, raycastMesh, dsFree, dsFitSphere, dsPickPoint } from './wasm.js';
|
||||
import { loadDatasetFiles, parseDatasetComponent } from './dataset.js';
|
||||
import { Renderer } from './renderer.js';
|
||||
import { Camera, Nav } from './camera.js';
|
||||
@@ -20,13 +20,15 @@ const opts = {
|
||||
primitive: 0, gamut: toColMajor(GAMUTS['Rec.709']), transfer: 0, isLinear: false,
|
||||
shDegree: 0, exposure: 1.0, opacityScale: 1.0,
|
||||
cameraModel: 'perspective', upAxis: 'z', showGrid: true, gridRadius: 1,
|
||||
// what the view orbits about (#center-mode; dsparse::CenterMode index)
|
||||
centerMode: 2,
|
||||
background: hexToRgb('0a0b0e'), shade: true, flatShade: false, meshColor: true,
|
||||
// dataset (point cloud + camera frustums)
|
||||
pointSize: 2.0, frustumScale: 1.0, showPoints: true, showFrustums: true, hoverCam: -1,
|
||||
};
|
||||
|
||||
// dataset session state (cameras metadata for hover/pick/view-from-camera)
|
||||
let dataset = null; // { cameras, components, token, fit, frustumBase, frustumMult, pickR }
|
||||
let dataset = null; // { cameras, components, token, frustumBase, frustumMult, pickR }
|
||||
|
||||
// Valid FOV range (degrees) per display camera model: tan blows up toward
|
||||
// 180° for the linear models; the fisheye projections are defined to 360°.
|
||||
@@ -129,6 +131,8 @@ window.__viewer = {
|
||||
snapshot: () => { if (model && model.type==='splat') maybeSort(true); renderer.render(camera, opts); return renderer.snapshot(); },
|
||||
get sortStats() { return sortStats; },
|
||||
get dataset() { return dataset; },
|
||||
// [cx, cy, cz, radius] of the loaded model about the chosen centering
|
||||
get fit() { return model && model.type === 'dataset' ? dsFitSphere(opts.centerMode) : fitSphere(opts.centerMode); },
|
||||
viewFromCamera: (i) => viewFromCamera(i),
|
||||
pickCamera: (px, py) => pickCamera(px, py),
|
||||
};
|
||||
@@ -161,8 +165,8 @@ function upTransform() {
|
||||
// outliers that make a bounding-box fit useless.
|
||||
function fitModel() {
|
||||
if (!model) return;
|
||||
if (model.type === 'dataset') { if (dataset) fitDataset(dataset.fit); return; }
|
||||
const fs = fitSphere(); // [cx,cy,cz, medianDist] (native frame)
|
||||
if (model.type === 'dataset') { if (dataset) fitDataset(dsFitSphere(opts.centerMode)); return; }
|
||||
const fs = fitSphere(opts.centerMode); // [cx,cy,cz, medianDist] (native frame)
|
||||
const c = mat3.mulVec(upTransform(), [fs[0], fs[1], fs[2]]);
|
||||
const r = 2.0 * fs[3] || 1;
|
||||
opts.gridRadius = r;
|
||||
@@ -177,6 +181,28 @@ function setUpAxis(axis) {
|
||||
lastSortDir = [0,0,0];
|
||||
dirty = true;
|
||||
}
|
||||
// Switch the centering: the model stays put and the orbit pivot moves to the
|
||||
// new centre, which is also what Reset View will fit to.
|
||||
function setCenterMode(mode) {
|
||||
mode = mode | 0;
|
||||
if (mode === opts.centerMode) return;
|
||||
opts.centerMode = mode;
|
||||
if (!model) return;
|
||||
const fs = model.type === 'dataset' ? dsFitSphere(mode) : fitSphere(mode);
|
||||
recenterAt(mat3.mulVec(upTransform(), [fs[0], fs[1], fs[2]]));
|
||||
}
|
||||
const CAMERA_CENTER_MODES = { 2: 1, 3: 4, 5: 4 }; // camera mode -> its point fallback
|
||||
// A file has no cameras, so the camera statistics are offered only over a
|
||||
// dataset; a selected one drops to the point statistic it would fall back to.
|
||||
function syncCenterMenu() {
|
||||
const sel = $('center-mode');
|
||||
const isDataset = !!(model && model.type === 'dataset');
|
||||
for (const o of sel.options) o.disabled = !isDataset && (+o.value in CAMERA_CENTER_MODES);
|
||||
if (!isDataset && opts.centerMode in CAMERA_CENTER_MODES) {
|
||||
opts.centerMode = CAMERA_CENTER_MODES[opts.centerMode];
|
||||
sel.value = String(opts.centerMode);
|
||||
}
|
||||
}
|
||||
|
||||
function forwardNative() {
|
||||
// camera forward in the model's native frame: U^T * forward_canonical
|
||||
@@ -350,11 +376,12 @@ async function loadSingleModel(entries) {
|
||||
model = { type:'mesh', nv: res.data.nv, nt: res.data.nt };
|
||||
showMeshUI(res.data);
|
||||
}
|
||||
syncCenterMenu();
|
||||
// Only fit the camera for the first model of the session: replacing the
|
||||
// model (e.g. dropping the mesh of the same object after a splat) keeps
|
||||
// the current view. The scene scale (move/zoom speed) is still refreshed.
|
||||
if (firstModel) fitModel();
|
||||
else { const fs = fitSphere(); nav._sceneScale = 2.0 * fs[3] || 1; opts.gridRadius = nav._sceneScale; }
|
||||
else { const fs = fitSphere(opts.centerMode); nav._sceneScale = 2.0 * fs[3] || 1; opts.gridRadius = nav._sceneScale; }
|
||||
lastSortDir = [0,0,0]; lastSortMode = -1;
|
||||
if (model.type === 'splat') maybeSort(true); // initial order, synchronous
|
||||
histCache.clear();
|
||||
@@ -381,7 +408,6 @@ async function loadDataset(entries, token) {
|
||||
// (dsPickPoint) still works; it is freed when a non-dataset model loads.
|
||||
sortGen++; sortPending = null; // invalidate any in-flight splat sort
|
||||
|
||||
const fit = res.fit;
|
||||
const frustumBase = res.frustumSize;
|
||||
const frustumMult = dataset ? dataset.frustumMult : 1.0;
|
||||
opts.frustumScale = frustumBase * frustumMult;
|
||||
@@ -389,12 +415,14 @@ async function loadDataset(entries, token) {
|
||||
cameras: res.cameras,
|
||||
components: res.components || (dataset && dataset.components) || [],
|
||||
token: res.selectedToken || token,
|
||||
fit, frustumBase, frustumMult,
|
||||
frustumBase, frustumMult,
|
||||
pickR: res.frustum.pickR, // size-1 per-camera frustum radii (ray picking)
|
||||
};
|
||||
model = { type:'dataset', numCameras: res.cameras.length, numPoints: res.points.count };
|
||||
opts.hoverCam = -1;
|
||||
showDatasetUI(res.summary);
|
||||
syncCenterMenu();
|
||||
const fit = dsFitSphere(opts.centerMode);
|
||||
if (firstDataset || token) fitDataset(fit); else nav._sceneScale = 2.0*(fit[3]||1);
|
||||
histCache.clear(); updateHistParams();
|
||||
$('drop-hint').style.display = 'none';
|
||||
@@ -875,7 +903,13 @@ function pinchState(pointers) {
|
||||
// UI control wiring
|
||||
// ---------------------------------------------------------------------------
|
||||
function wireControls() {
|
||||
const bind = (id, ev, fn) => $(id).addEventListener(ev, fn);
|
||||
// A host page may carry a subset of the controls (viewer/test/test_ds.html).
|
||||
// Throwing here would leave every control wired after it dead instead.
|
||||
const bind = (id, ev, fn) => {
|
||||
const el = $(id);
|
||||
if (!el) { console.warn('no control #' + id); return; }
|
||||
el.addEventListener(ev, fn);
|
||||
};
|
||||
bind('nav-mode','change', e => nav.mode = e.target.value);
|
||||
bind('move-speed','input', e => { nav.speedExp = +e.target.value; $('v-speed').textContent = Math.pow(10,+e.target.value).toFixed(2)+'×'; });
|
||||
bind('btn-reset','click', () => { if (model) fitModel(); else nav.reset(); });
|
||||
@@ -914,6 +948,7 @@ function wireControls() {
|
||||
});
|
||||
bind('fov','input', e => { camera.fov = +e.target.value*Math.PI/180; fovMemory[camera.model] = camera.fov; $('v-fov').textContent = e.target.value+'°'; dirty=true; });
|
||||
bind('up-axis','change', e => setUpAxis(e.target.checked ? 'y' : 'z'));
|
||||
bind('center-mode','change', e => setCenterMode(e.target.value));
|
||||
bind('flat-shade','change', e => { opts.flatShade = e.target.checked; dirty=true; });
|
||||
bind('mesh-color','change', e => { opts.meshColor = e.target.checked; dirty=true; });
|
||||
bind('grid','change', e => { opts.showGrid = e.target.checked; dirty=true; });
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
+4
-3
@@ -421,8 +421,9 @@ export function meshHistogram(param, nbins) {
|
||||
}
|
||||
export function meshEdgeCount() { return call('ssv_mesh_edge_count'); }
|
||||
export function bbox() { return f32(call('ssv_bbox'), 6).slice(); }
|
||||
// robust fit sphere: [cx, cy, cz, medianDistance]
|
||||
export function fitSphere() { return f32(call('ssv_fit_sphere'), 4).slice(); }
|
||||
// fit sphere about centering mode `mode` (index.html #center-mode):
|
||||
// [cx, cy, cz, medianDistance]
|
||||
export function fitSphere(mode = 2) { return f32(call('ssv_fit_sphere', 'number', ['number'], [mode|0]), 4).slice(); }
|
||||
// nearest ray/mesh hit distance (model-native frame), or -1
|
||||
export function raycastMesh(ox, oy, oz, dx, dy, dz) {
|
||||
return call('ssv_raycast_mesh','number',
|
||||
@@ -530,7 +531,7 @@ export function dsSummary() {
|
||||
export function dsLastError() {
|
||||
return Module.ccall('ssv_ds_last_error', 'string', [], []);
|
||||
}
|
||||
export function dsFitSphere() { return f32(call('ssv_ds_fit_sphere') >>> 0, 4).slice(); }
|
||||
export function dsFitSphere(mode = 2) { return f32(call('ssv_ds_fit_sphere', 'number', ['number'], [mode|0]) >>> 0, 4).slice(); }
|
||||
export function dsFrustumSize() { return call('ssv_ds_frustum_size'); }
|
||||
export function dsPickPoint(ox, oy, oz, dx, dy, dz) {
|
||||
const ptr = Module.ccall('ssv_ds_pick_point', 'number',
|
||||
|
||||
@@ -44,11 +44,14 @@ static std::string g_format; // "COLMAP" / "Nerfstudio" / "Metashape" / "
|
||||
static std::string g_json; // reused output buffer (enumerate / summary)
|
||||
static std::string g_names; // newline-joined image basenames
|
||||
static std::string g_error; // last parse error ("" = clean load)
|
||||
static std::vector<float> g_pts; // the seed cloud as JS reads it, [N, 3]
|
||||
static float g_fit[4]; // (cx, cy, cz, radius)
|
||||
static float g_pick[3]; // (index, t, perp) for point picking
|
||||
|
||||
static void reset_result() {
|
||||
g_ds = ParsedDataset();
|
||||
g_pts.clear();
|
||||
g_pts.shrink_to_fit();
|
||||
g_loaded = false;
|
||||
g_format.clear();
|
||||
g_names.clear();
|
||||
@@ -364,7 +367,12 @@ KEEP int ssv_ds_parse(const char* token_c) {
|
||||
|
||||
g_ds.num_cameras = (int64_t)g_ds.camera_models.size();
|
||||
build_names();
|
||||
g_loaded = (g_ds.num_cameras > 0 || g_ds.points.num() > 0);
|
||||
// The parsers keep the cloud in double; JS wants one Float32Array view of
|
||||
// it, and the double copy has no reader left once that exists.
|
||||
g_pts.assign(g_ds.points.xyz.begin(), g_ds.points.xyz.end());
|
||||
g_ds.points.xyz.clear();
|
||||
g_ds.points.xyz.shrink_to_fit();
|
||||
g_loaded = (g_ds.num_cameras > 0 || g_pts.size() > 0);
|
||||
return g_loaded ? 1 : 0;
|
||||
}
|
||||
|
||||
@@ -375,8 +383,8 @@ KEEP int ssv_ds_ok() { return g_loaded ? 1 : 0; }
|
||||
KEEP char* ssv_ds_json() { return (char*)g_json.c_str(); }
|
||||
KEEP char* ssv_ds_last_error() { return (char*)g_error.c_str(); }
|
||||
|
||||
KEEP int ssv_ds_num_points() { return (int)g_ds.points.num(); }
|
||||
KEEP float* ssv_ds_points_xyz() { return g_ds.points.xyz.empty()? nullptr : g_ds.points.xyz.data(); }
|
||||
KEEP int ssv_ds_num_points() { return (int)(g_pts.size() / 3); }
|
||||
KEEP float* ssv_ds_points_xyz() { return g_pts.empty()? nullptr : g_pts.data(); }
|
||||
KEEP uint8_t* ssv_ds_points_rgb() { return g_ds.points.rgb.empty()? nullptr : g_ds.points.rgb.data(); }
|
||||
|
||||
KEEP int ssv_ds_num_cameras() { return (int)g_ds.num_cameras; }
|
||||
@@ -447,39 +455,36 @@ KEEP char* ssv_ds_summary_json() {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Robust fit sphere over points + camera centers (median center, median
|
||||
// distance) -> (cx, cy, cz, radius). Mirrors ssv_fit_sphere in viewer.cpp.
|
||||
// Fit sphere: the dsparse::CenterMode `mode` over points + cameras, and a
|
||||
// radius about it -> (cx, cy, cz, radius). Mirrors ssv_fit_sphere in
|
||||
// viewer.cpp.
|
||||
// ---------------------------------------------------------------------------
|
||||
KEEP float* ssv_ds_fit_sphere() {
|
||||
std::vector<float> xs, ys, zs; // combined (for a robust center)
|
||||
KEEP float* ssv_ds_fit_sphere(int mode) {
|
||||
std::vector<float> pd, cd; // squared distances: points / cameras
|
||||
const auto& P = g_ds.points.xyz;
|
||||
int64_t np = g_ds.points.num();
|
||||
int64_t step = np > (1<<20) ? np / (1<<20) : 1; if (step < 1) step = 1;
|
||||
for (int64_t i = 0; i < np; i += step) {
|
||||
xs.push_back(P[i*3]); ys.push_back(P[i*3+1]); zs.push_back(P[i*3+2]);
|
||||
const int64_t np = (int64_t)g_pts.size() / 3;
|
||||
const int64_t ncam = std::min<int64_t>(g_ds.num_cameras, (int64_t)g_ds.c2w.size() / 12);
|
||||
if (np == 0 && ncam == 0) { g_fit[0]=g_fit[1]=g_fit[2]=0.f; g_fit[3]=1.f; return g_fit; }
|
||||
{
|
||||
std::vector<double> c2w(g_ds.c2w.begin(), g_ds.c2w.begin() + ncam * 12);
|
||||
mode = std::max(0, std::min(mode, dsparse::kNumCenterModes - 1));
|
||||
const std::array<double, 3> c = dsparse::scene_center(
|
||||
(dsparse::CenterMode)mode, c2w.data(), ncam, g_pts.data(), np, 3, 1 << 20);
|
||||
for (int k = 0; k < 3; k++) g_fit[k] = (float)c[k];
|
||||
}
|
||||
// camera centers = translation column of c2w [N,3,4]
|
||||
int ncam = 0;
|
||||
for (int i = 0; i < (int)g_ds.num_cameras; i++) {
|
||||
if (12*i+11 >= (int)g_ds.c2w.size()) break;
|
||||
xs.push_back(g_ds.c2w[12*i+3]);
|
||||
ys.push_back(g_ds.c2w[12*i+7]);
|
||||
zs.push_back(g_ds.c2w[12*i+11]);
|
||||
ncam++;
|
||||
}
|
||||
if (xs.empty()) { g_fit[0]=g_fit[1]=g_fit[2]=0.f; g_fit[3]=1.f; return g_fit; }
|
||||
auto quantile = [](std::vector<float>& v, float q)->float{
|
||||
if (v.empty()) return 0.f;
|
||||
size_t k = (size_t)(q * (v.size()-1));
|
||||
std::nth_element(v.begin(), v.begin()+k, v.end());
|
||||
return v[k];
|
||||
};
|
||||
g_fit[0]=quantile(xs,0.5f); g_fit[1]=quantile(ys,0.5f); g_fit[2]=quantile(zs,0.5f);
|
||||
size_t nps = xs.size() - ncam;
|
||||
for (size_t i = 0; i < xs.size(); i++) {
|
||||
float dx=xs[i]-g_fit[0], dy=ys[i]-g_fit[1], dz=zs[i]-g_fit[2];
|
||||
(i < nps ? pd : cd).push_back(dx*dx+dy*dy+dz*dz);
|
||||
int64_t step = np > (1<<20) ? np / (1<<20) : 1; if (step < 1) step = 1;
|
||||
for (int64_t i = 0; i < np; i += step) {
|
||||
float dx=g_pts[i*3]-g_fit[0], dy=g_pts[i*3+1]-g_fit[1], dz=g_pts[i*3+2]-g_fit[2];
|
||||
pd.push_back(dx*dx+dy*dy+dz*dz);
|
||||
}
|
||||
for (int64_t i = 0; i < ncam; i++) {
|
||||
float dx=g_ds.c2w[12*i+3]-g_fit[0], dy=g_ds.c2w[12*i+7]-g_fit[1], dz=g_ds.c2w[12*i+11]-g_fit[2];
|
||||
cd.push_back(dx*dx+dy*dy+dz*dz);
|
||||
}
|
||||
// Frame the capture volume: points are dense (median), cameras are the
|
||||
// outer boundary we still want in view (90th percentile, robust to a stray
|
||||
@@ -502,8 +507,8 @@ KEEP float ssv_ds_frustum_size() {
|
||||
KEEP float* ssv_ds_pick_point(float ox, float oy, float oz,
|
||||
float dx, float dy, float dz) {
|
||||
g_pick[0] = -1.f; g_pick[1] = 0.f; g_pick[2] = 3.4e38f;
|
||||
const auto& P = g_ds.points.xyz;
|
||||
int64_t np = g_ds.points.num();
|
||||
const auto& P = g_pts;
|
||||
int64_t np = (int64_t)g_pts.size() / 3;
|
||||
float dd = dx*dx + dy*dy + dz*dz; if (dd < 1e-20f) return g_pick;
|
||||
float best_score = 3.4e38f;
|
||||
for (int64_t i = 0; i < np; i++) {
|
||||
|
||||
+9
-12
@@ -29,6 +29,7 @@
|
||||
// strtod/strtof cost ~1.5us per call — tens of seconds over a 100+ MB OBJ
|
||||
// or ascii PLY.
|
||||
#include "data/FastFloat.h"
|
||||
#include "data/SceneCenter.h"
|
||||
|
||||
#define KEEP EMSCRIPTEN_KEEPALIVE extern "C"
|
||||
|
||||
@@ -1255,12 +1256,11 @@ KEEP float* ssv_bbox(){
|
||||
return g_bbox;
|
||||
}
|
||||
|
||||
// Robust fitting sphere of the current model: per-axis median center and the
|
||||
// median distance from it -> 4 floats (cx, cy, cz, median_dist). Trained
|
||||
// splats and generated meshes often contain far-away outliers that blow up a
|
||||
// bounding-box fit; medians ignore them. Subsamples uniformly for speed.
|
||||
// (cx, cy, cz, median_dist) about the dsparse::CenterMode `mode`; a file has
|
||||
// no cameras, so the camera modes fall back to the point ones. Medians,
|
||||
// because trained splats and meshes carry far-away outliers a box fit cannot.
|
||||
static float g_fitsph[4];
|
||||
KEEP float* ssv_fit_sphere(){
|
||||
KEEP float* ssv_fit_sphere(int mode){
|
||||
const float* P = nullptr; uint32_t N = 0, stride = 3;
|
||||
if (g_last_kind == 1) { P = g_splat.posop.data(); N = g_splat.count; stride = 4; }
|
||||
else if (g_last_kind == 2) { P = g_mesh.pos.data(); N = g_mesh.nv; stride = 3; }
|
||||
@@ -1268,14 +1268,11 @@ KEEP float* ssv_fit_sphere(){
|
||||
const uint32_t MAXS = 1u<<20;
|
||||
uint32_t step = (N + MAXS - 1) / MAXS; if (step < 1) step = 1;
|
||||
uint32_t M = (N + step - 1) / step;
|
||||
mode = std::max(0, std::min(mode, dsparse::kNumCenterModes - 1));
|
||||
const std::array<double, 3> c = dsparse::scene_center(
|
||||
(dsparse::CenterMode)mode, nullptr, 0, P, (int64_t)N, (int)stride, (int64_t)MAXS);
|
||||
for (int k = 0; k < 3; k++) g_fitsph[k] = (float)c[k];
|
||||
std::vector<float> tmp; tmp.reserve(M);
|
||||
for (int k = 0; k < 3; k++) {
|
||||
tmp.clear();
|
||||
for (uint32_t i = 0; i < N; i += step) tmp.push_back(P[(size_t)i*stride+k]);
|
||||
std::nth_element(tmp.begin(), tmp.begin()+tmp.size()/2, tmp.end());
|
||||
g_fitsph[k] = tmp[tmp.size()/2];
|
||||
}
|
||||
tmp.clear();
|
||||
for (uint32_t i = 0; i < N; i += step) {
|
||||
float dx=P[(size_t)i*stride]-g_fitsph[0], dy=P[(size_t)i*stride+1]-g_fitsph[1], dz=P[(size_t)i*stride+2]-g_fitsph[2];
|
||||
tmp.push_back(dx*dx+dy*dy+dz*dz);
|
||||
|
||||
Reference in New Issue
Block a user