recategorize training configs

This commit is contained in:
Harry Chen
2026-08-06 23:57:16 -04:00
parent afe17a6b80
commit 5acac8589e
13 changed files with 882 additions and 504 deletions
+5 -1
View File
@@ -204,7 +204,11 @@ Rules:
training config, and it is hand-written, not generated. Adding a row to
`SS_CONFIG_FIELDS` makes the field appear in the native CLI, `--help`,
the GUI's "All Options" editor, the run's `config.json` and `TrainerCore` —
the struct is expanded from the same table, so the two cannot drift.
the struct is expanded from the same table, so the two cannot drift. A row
also carries a `section` (which heading it is listed under) and a `tier`
(`basic` / `advanced` / `expert` / `stub`, which decides whether `--help`
and the GUI's default view show it at all); both are display metadata, and
`config.json` is flat so neither reaches disk.
5. `.cuh` declaration sections must stay CUDA-include-free — they have to
parse under `-DSS_BACKEND_VULKAN` without the CUDA toolkit.
+7 -5
View File
@@ -107,7 +107,7 @@ rather than instantiating everything in one TU.
source of truth. It holds:
- `SS_CONFIG_FIELDS(X)` — one X-macro row per flag,
`(type, member, default, group, choices, help)`;
`(type, member, default, section, tier, choices, help)`;
- `struct TrainConfig` — **expanded from that same table**, so a field cannot
exist in one and not the other;
- `kTrainPresets` + `train_apply_preset()` — one branch per preset.
@@ -118,10 +118,12 @@ path's `config.json` reader. Add a row and the flag appears in all of them.
The CLI flag is `member` stringified (`--sh-degree` sets `sh_degree`; `-` and
`_` are interchangeable), so a flag name cannot drift from its member. The
`config.json` key is `train_json_key(flag)`, which is the identity for
everything except `dm_split_batch` → `split_batch`; that shim exists because
`config.json` is read back by `spirula mesh` and `--resume`, and the
datamanager field would otherwise collide with `model.split_batch`.
`config.json` key is the flag, spelled the same way, at the top level: the
file is flat. It used to nest under the `group` column, which made a
presentational choice part of an on-disk format — moving a flag to another
heading moved its key, and the reader (`spirula mesh`, `--resume`) silently
fell back to the default when it could not find it. `section` and `tier` are
display metadata only and can be reshuffled freely.
This used to be generated from the Python dataclasses by
`generate_cli_config.py`. It isn't any more — the Python dataclasses are
+27 -16
View File
@@ -72,16 +72,17 @@ Design: novice path is Home → "Open a Dataset" (or "Create Dataset from
Photos/Video", which drives external `colmap`/`ffmpeg` CLIs with live log +
cancel) → preset dropdown + a curated Basic Options list → Start Training →
live native viewport. Advanced path: "All Options" editor **generated from
the `SS_CONFIG_FIELDS` X-macro** — all config fields, grouped by
sub-config, searchable, Python docstrings as tooltips, modified-from-preset
highlighting with right-click reset; new Python config fields appear
automatically after codegen. Web viewer can be additionally served from the
the `SS_CONFIG_FIELDS` X-macro** — all config fields, under the field
table's `section` headings, filtered by its `tier` (Basic / Advanced /
Everything), searchable across the filter, help text as tooltips,
modified-from-preset highlighting with right-click reset; a new row in the
field table appears automatically. Web viewer can be additionally served from the
GUI (Basic Options) for remote monitoring.
| File | Role |
|---|---|
| `gui/GuiMain.cpp` | GLFW window + GL 3.2 core context + ImGui bootstrap, dark style, DPI scale, frame loop, close-confirm flow. **Drag-and-drop** (glfwSetDropCallback → `GuiApp::handle_drop`, which takes the **whole drop at once** — several videos dropped together are the inputs of one dataset, and `spirula <path>...` goes through the same call): each path is auto-detected as an SfM dataset folder (transforms.json / sparse/ / colmap/ marker, or a Metashape camera .xml + point-cloud .ply pair → open; only honoured when dropped alone), a photo folder (contains images), or a video file (extension; the .insv preset applies per file). Files from inside a dataset (transforms.json, .db/.bin/.txt/.xml) open their parent. Raw input dropped onto the dataset screen is **added** to the list there; dropped anywhere else it starts a new dataset. Video/photo drops are ignored while training (datasets go through the stop-confirm flow). |
| `gui/GuiApp.h/.cpp` | Screens (Home / COLMAP / Train), layout, wiring; recents + tool paths persisted to `~/.config/spirula-studio/gui.conf` (`%APPDATA%` on Windows). Session-destroying navigation (Home / open-dataset / quit during training) goes through a stop-and-save confirm modal with a deferred pending-action; output folder defaults to `<dataset>/outputs` with a Browse button + resolved-run-path preview. Editing any dataset-parsing option (dataparser group via the generated `parse_settings_equal`, plus warp/load/scale flags) marks the dataset dirty and auto-reloads it once the edited widget loses focus. NOTE: `open_dataset`/`add_recent` take `std::string` **by value** — callers pass `_recents` elements and `add_recent` mutates that vector (a const& dangles; this was a real bug that corrupted the path to ""). |
| `gui/GuiApp.h/.cpp` | Screens (Home / COLMAP / Train), layout, wiring; recents + tool paths persisted to `~/.config/spirula-studio/gui.conf` (`%APPDATA%` on Windows). Session-destroying navigation (Home / open-dataset / quit during training) goes through a stop-and-save confirm modal with a deferred pending-action; output folder defaults to `<dataset>/outputs` with a Browse button + resolved-run-path preview. Editing any dataset-parsing option (`SS_DATASET_PARSE_FIELDS` via the generated `parse_settings_equal`) marks the dataset dirty and auto-reloads it once the edited widget loses focus. NOTE: `open_dataset`/`add_recent` take `std::string` **by value** — callers pass `_recents` elements and `add_recent` mutates that vector (a const& dangles; this was a real bug that corrupted the path to ""). |
| `gui/ConfigUI.h/.cpp` | The X-macro-generated options editor. Zero per-field special cases (only `data` is hidden, managed by the dataset picker). |
| `gui/ViewportPanel.h/.cpp` | Native viewport with two backends behind the browser-identical NavCamera navigation: **Preview** = GL point cloud + frusta as soon as the dataset parses (PreviewRenderer), **Engine** = RenderWorker once training starts, with all four web-viewer camera models (Pinhole / Fisheye-equidistant / Fisheye-equisolid / Equirectangular; `fovToIntrinsics` + per-model FOV ranges ported from viewer.html). Buffer picker, camera-frusta overlay with a live **frustum-size slider** (`ViewRequest::cam_size_scale`), render-scale + live-refresh throttle (0.15 s). Initial framing: seed-point centroid target, median camera distance, camera-centroid direction. **Double-click centering** (viewer.html `recenterAt`): pan laterally so the 3D point under the cursor sits on the optical axis and make it the orbit pivot (rotate toward it when behind the camera plane, >180° models); preview mode picks the nearest displayed point along the cursor ray (3% angular cone, `PreviewRenderer::pick_point`), engine mode attaches `pick_px/py` to the next render and gets the point back in the ViewResult — depth-channel readback, no extra VRAM or render pass; all four display camera models via `viewer_pixel_ray`. |
| `gui/NavCamera.h/.cpp` | 1:1 port of viewer.html's `cam`/`quat`/`Nav`: quaternion camera, four modes (Turntable / Trackball / First Person / Free Fly), same sensitivities and mappings for **mouse** (LMB orbit-or-look, RMB/MMB/Shift pan, wheel dolly), **keyboard** (WASD/arrows, E/Q up-down or Fly-roll, active while the pointer is over the viewport), **gamepad** (GLFW gamepad API: left stick move, right stick look, triggers up-down/roll -- including the browser quirk that triggers only translate while the left stick is deflected), and **touch** via the OS's pointer/gesture emulation (single finger = orbit; system pinch/pan gestures arrive as wheel; GLFW exposes no raw multitouch). Keep in sync with viewer.html's Nav. The initial pose replicates `cam.reset()` verbatim (target = client-frame origin = the CAMERA-POSE center via center_method="poses", pos=[0,0,1], orbit(0,-250)) -- verified pixel-equivalent against a `/render` fetch from `spirula train`'s web viewer at the client's default c2w. `SS_NAV_DEBUG=1` logs per-frame mouse-drag nav decisions (button/pan/target/pos) to stderr. |
@@ -128,7 +129,7 @@ extraction (see below).
| `WriterPool.h` | Bounded-queue worker threads that JPEG/PNG-encode and write frames and masks off the calling thread. Used by `FrameExtract`, `spirula sam track` and the GUI's folder-masking loop. Encoding a 1080p mask through stb's deflate is ~75 ms — a third of a SAM 2.1 Tiny frame — and none of it needs the GPU, so a caller that writes inline sets the frame rate with zlib. The queue bound is what keeps a slow disk applying back-pressure instead of growing until memory runs out. |
| `HttpServer.h/.cpp` | Minimal HTTP/1.0 GET server (POSIX sockets; winsock shim compiles but untested). Serial request handling — parity with Python's non-threading `HTTPServer`. |
| `Viewer.h/.cpp` | Web-viewer server port (viewer/server.py + http_server.py + render_worker.py + annotation.py): latest-wins render worker, `get_outputs` viewer subset, `engine_blit_view` GPU annotation/colormap, stb JPEG encode. Serves the **unchanged** `viewer.html` (embedded at configure time via CMake hex; `SS_VIEWER_HTML=<path>` env overrides for dev). `/pick?px=&py=&<camera params>` returns the 3D point under a pixel as JSON for viewer.html's double-click centering (the Python server has no /pick; the client treats non-OK responses as a no-op). |
| `../config/TrainConfig.h` | Hand-written, the training config's single source of truth: the `SS_CONFIG_FIELDS(X)` X-macro flag table (190 rows), `struct TrainConfig` expanded from it, `kTrainPresets` and `train_apply_preset()`. |
| `../config/TrainConfig.h` | Hand-written, the training config's single source of truth: the `SS_CONFIG_FIELDS(X)` X-macro flag table (178 rows), `struct TrainConfig` expanded from it, `SS_DATASET_PARSE_FIELDS`, `kTrainPresets` + `train_apply_preset()`, and `train_resolve_macros()`. |
| `../external/` | All vendored third-party code (marked `linguist-vendored` in `.gitattributes` along with the generated dirs): `stb_image.h`/`stb_image_write.h` (images), `npy.hpp` (checkpoints), `miniz.c/.h` (zip reading for the Metashape `.psx` camera table; compiled into `spirula` only). |
Debug: `SS_DUMP_CAMERAS=<path> spirula train ...` dumps parsed + post-split
@@ -168,19 +169,29 @@ HTTP/viewer.
## The config table (source of truth = `src/config/TrainConfig.h`)
Hand-written, one `SS_CONFIG_FIELDS` row per flag:
`X(type, member, default, group, choices, help)`. `struct TrainConfig` is
`X(type, member, default, section, tier, choices, help)`. `struct TrainConfig` is
expanded from the same table, so the declaration and the metadata cannot
drift. Add a row and the flag appears in the CLI parser, `--help`, the GUI's
"All Options" editor, `config.json` and the pybind module.
- **Flag names**: `member` stringified. `-` and `_` are interchangeable, so
`--sh-degree` sets `sh_degree`. A flag cannot drift from its member.
- **`config.json` keys**: `train_json_key(flag)`, the identity except
`dm_split_batch` → `split_batch`. `config.json` is read back by
`spirula mesh` and `--resume`, and the datamanager field would otherwise
collide with `model.split_batch` (datamanager's is the legacy Python-path
OOM workaround, a no-op on the managed path). New fields never need an
entry — the shim is compatibility, not a mechanism.
- **`config.json` keys**: the flag name, at the top level — the file is
flat. It used to nest under `group`, which quietly made a presentational
choice part of an on-disk format: move a flag to another heading and its
key moved with it, and the reader (`spirula mesh`, `--resume`) fell back to
the default without saying so. `section` and `tier` never reach disk.
- **`section` / `tier`**: which heading a flag is listed under, and how
specialist it is (`basic` / `advanced` / `expert` / `stub`). `--help` shows
the basic ones and points at `--help-all`; the GUI has the same filter as a
dropdown. Rows must stay contiguous per section — both consumers stream
headings as they walk the table.
- **Macro options**: `quality`, `floater_suppression`,
`distraction_robustness` are ordinary rows that stand in for several
specialist flags each. `train_resolve_macros()` applies them after the
preset and never over a flag the user set by hand; a macro at its default
writes nothing. The CLI passes its `seen` set, the GUI its
`ConfigUIState::touched`.
- **Commas**: macro arguments split on them, so `std::array<T, N>` fields use
the `TrainVec3i` / `TrainVec3f` aliases and the `train_v3i()` /
`train_v3f()` makers.
@@ -275,9 +286,9 @@ without the other and that gate fails.
## TODOs (rough priority)
1. **Eval pass + metrics** — iterate `next_val_batch` / render train views,
PSNR/SSIM from engine buffers; then `validation_fraction` early-stop
(model config `overfit_score_*`, `early_stop_*` fields are parsed but
unused).
PSNR/SSIM from engine buffers; then `validation_fraction` early-stop (the
`overfit_score_*` / `early_stop_*` fields were parsed but unused and have
been removed; re-add them when the pass lands).
2. **Resume** — `engine_load_checkpoint` after skeleton setup
(trainer.py:132-137); config.json round-trip (CLI dump is close to but
not tyro-compatible; decide on a shared format).
+11 -21
View File
@@ -476,34 +476,24 @@ template <typename T, size_t N> std::string json_str(const std::array<T, N>& v)
} // namespace
// Nested-by-group dump. Keys are train_json_key(flag) -- see the note on it
// in config/TrainConfig.h; this is a read-back format (`spirula mesh` and
// --resume parse it), so the key set is not free to change.
// 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.
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");
if (!f) throw std::runtime_error("cannot write config.json");
std::fprintf(f, "{\n \"preset\": \"%s\"", preset.c_str());
const char* open_group = "";
#define SS_DUMP(type, member, default_, group, choices, help) \
{ \
const char* key = train_json_key(#member); \
if (std::strcmp(group, "trainer") == 0) { \
std::fprintf(f, ",\n \"%s\": %s", key, json_str(c.member).c_str()); \
} else { \
if (std::strcmp(open_group, group) != 0) { \
if (*open_group) std::fprintf(f, "\n }"); \
std::fprintf(f, ",\n \"%s\": {", group); \
open_group = group; \
std::fprintf(f, "\n \"%s\": %s", key, json_str(c.member).c_str()); \
} else { \
std::fprintf(f, ",\n \"%s\": %s", key, json_str(c.member).c_str()); \
} \
} \
}
#define SS_DUMP(type, member, default_, section, tier, choices, help) \
std::fprintf(f, ",\n \"%s\": %s", #member, json_str(c.member).c_str());
SS_CONFIG_FIELDS(SS_DUMP)
#undef SS_DUMP
if (*open_group) std::fprintf(f, "\n }");
std::fprintf(f, "\n}\n");
std::fclose(f);
}
+47 -9
View File
@@ -127,7 +127,7 @@ void check_choices(const T&, const std::string&, const char*) {}
bool set_config_field(TrainConfig& c, const std::string& key,
int argc, char** argv, int& i,
std::set<std::string>& seen) {
#define SS_TRY_SET(type, member, default_, group, choices, help) \
#define SS_TRY_SET(type, member, default_, section, tier, choices, help) \
if (key == #member) { \
consume(c.member, key, argc, argv, i); \
check_choices(c.member, key, choices); \
@@ -201,13 +201,19 @@ void select_and_print_devices(const std::string& requested) {
std::fflush(stdout);
}
void print_help(const char* argv0, const TrainConfig& c) {
// `max_tier` is a rank into kTrainTiers: 0 lists only the flags a first run
// needs (--help), kTrainNumTiers-1 lists every one of them (--help-all).
void print_help(const char* argv0, const TrainConfig& c, int max_tier) {
std::printf("usage: %s [<preset>] --data <dataset_dir> [--flag value ...]\n\n", argv0);
std::printf("presets (tyro subcommands; default: 3dgs):\n");
static_assert(sizeof(kTrainPresets) / sizeof(kTrainPresets[0]) ==
spirula::i18n::msg::train::kNumPresetText,
"config/TrainConfig.h and i18n/catalog/Train.h disagree "
"about how many presets there are");
static_assert((size_t)kTrainNumSections ==
spirula::i18n::msg::train::kNumSectionText,
"config/TrainConfig.h and i18n/catalog/Train.h disagree "
"about how many section headings there are");
for (const auto& p : kTrainPresets) {
const auto* t = spirula::i18n::msg::train::preset_text(p.name);
std::printf(" %-18s %s\n", p.name, t ? t->help->get() : "");
@@ -218,13 +224,28 @@ void print_help(const char* argv0, const TrainConfig& c) {
" list prints at startup.\n");
std::printf("\nflags ('-' and '_' interchangeable; bools take 0/1; 'none' clears "
"optional values;\n defaults shown for the selected preset):\n");
const char* cur_group = "";
#define SS_PRINT_HELP(type, member, default_, group, choices, help) \
if (std::strcmp(cur_group, group) != 0) { \
cur_group = group; \
std::printf("\n [%s]\n", group); \
// Pass 1: how many flags each heading has left after the tier filter, so
// a heading with nothing under it is not printed at all.
int hidden = 0;
int vis[kTrainNumSections] = {0};
#define SS_COUNT_HELP(type, member, default_, section, tier, choices, help) \
if (train_tier_rank(tier) <= max_tier) vis[train_section_index(section)]++; \
else hidden++;
SS_CONFIG_FIELDS(SS_COUNT_HELP)
#undef SS_COUNT_HELP
const char* cur_section = "";
#define SS_PRINT_HELP(type, member, default_, section, tier, choices, help) \
if (std::strcmp(cur_section, section) != 0) { \
cur_section = section; \
if (vis[train_section_index(section)]) { \
const spirula::i18n::Msg* label = \
spirula::i18n::msg::train::section_label(section); \
std::printf("\n [%s]\n", label ? label->get() : section); \
} \
} \
{ \
if (train_tier_rank(tier) <= max_tier) { \
std::string h = help; \
size_t dot = h.find(". "); \
if (dot != std::string::npos) h = h.substr(0, dot + 1); \
@@ -239,6 +260,10 @@ void print_help(const char* argv0, const TrainConfig& c) {
}
SS_CONFIG_FIELDS(SS_PRINT_HELP)
#undef SS_PRINT_HELP
if (hidden)
std::printf("\n%d more flags, for tuning rather than for getting a "
"first result: --help-all\n", hidden);
}
@@ -314,7 +339,14 @@ int spirula_train_main(int argc, char** argv) {
std::string device_flag;
for (int i = argi; i < argc; i++) {
std::string arg = argv[i];
if (arg == "--help" || arg == "-h") { print_help(argv[0], cfg); return 0; }
if (arg == "--help" || arg == "-h") {
print_help(argv[0], cfg, 0);
return 0;
}
if (arg == "--help-all" || arg == "--help_all") {
print_help(argv[0], cfg, kTrainNumTiers - 1);
return 0;
}
if (arg.rfind("--", 0) != 0)
throw std::runtime_error("unexpected argument: " + arg + " (flags are --key value)");
// App-level flag, not part of the generated training config.
@@ -343,6 +375,12 @@ int spirula_train_main(int argc, char** argv) {
" (see --help for the full list)");
}
// ---- Macro options -------------------------------------------------
// --quality and friends stand in for a handful of flags each; they
// fill in only the ones this command line left alone. What they write
// joins `seen`, so a macro passed here still beats a --resume base.
train_resolve_macros(cfg, seen, &seen);
// ---- Resume --------------------------------------------------------
// The checkpoint's config.json becomes the base: it carries the
// architecture and data config the saved engine state was built for.
+7 -10
View File
@@ -56,10 +56,9 @@ MeshCameras load_cameras(const JsonValue& run_cfg, const std::string& data_dir,
const std::string& data_format_override) {
DatasetParserConfig pcfg;
std::string data_format = data_format_override;
const JsonValue* dp = run_cfg.find("dataparser");
// config.json is flat: one key per training flag, spelled as the flag is.
auto dp_str = [&](const char* key, const std::string& def) {
if (!dp) return def;
const JsonValue* v = dp->find(key);
const JsonValue* v = run_cfg.find(key);
return (v && !v->is_null()) ? v->as_string() : def;
};
pcfg.recon_dir = dp_str("colmap_recon_dir", "");
@@ -69,12 +68,12 @@ 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");
if (dp) {
const JsonValue* v = dp->find("rescale_camera_to_fit");
{
const JsonValue* v = run_cfg.find("rescale_camera_to_fit");
// bool auto-detect is unported; a number divides intrinsics
if (v && v->type == JsonValue::Type::Number)
pcfg.rescale_camera_to_fit = (float)v->as_double(0.0);
const JsonValue* fmt = dp->find("data_format");
const JsonValue* fmt = run_cfg.find("data_format");
if (data_format.empty() && fmt && !fmt->is_null()) data_format = fmt->as_string();
}
// Use every parsed frame (train + would-be eval): more cameras only make
@@ -89,10 +88,8 @@ MeshCameras load_cameras(const JsonValue& run_cfg, const std::string& data_dir,
// relative_scale: splats live in a frame scaled by this (model.py:561);
// scale the c2w translations to match before inverting.
float rel = 1.0f;
if (const JsonValue* model = run_cfg.find("model")) {
const JsonValue* rs = model->find("relative_scale");
if (rs && rs->type == JsonValue::Type::Number) rel = (float)rs->as_double(1.0);
}
if (const JsonValue* rs = run_cfg.find("relative_scale"))
if (rs->type == JsonValue::Type::Number) rel = (float)rs->as_double(1.0);
if (rel != 1.0f)
for (int64_t i = 0; i < C; ++i)
for (int r = 0; r < 3; ++r)
+57 -31
View File
@@ -8,6 +8,7 @@
#include "app/gui/Ui.h"
#include "i18n/catalog/Gui.h"
#include "i18n/catalog/Train.h"
#include "imgui.h"
#include "imgui_stdlib.h"
@@ -206,23 +207,26 @@ bool field_row(const char* cli_key, T& v, const T& def,
return changed;
}
int group_index(const char* g) {
if (!std::strcmp(g, "trainer")) return 0;
if (!std::strcmp(g, "dataparser")) return 1;
if (!std::strcmp(g, "datamanager")) return 2;
if (!std::strcmp(g, "model")) return 3;
return 4; // optimizer
// The heading text is shared with `spirula train --help`, so it lives in the
// train catalog rather than this file's.
const Msg& section_label(const char* section) {
static_assert((size_t)kTrainNumSections ==
spirula::i18n::msg::train::kNumSectionText,
"config/TrainConfig.h and i18n/catalog/Train.h disagree "
"about how many section headings there are");
const Msg* m = spirula::i18n::msg::train::section_label(section);
return m ? *m : msg::cfg_no_description;
}
const Msg& group_label(const char* g) {
switch (group_index(g)) {
case 0: return msg::cfg_group_run;
case 1: return msg::cfg_group_dataparser;
case 2: return msg::cfg_group_datamanager;
case 3: return msg::cfg_group_model;
default: return msg::cfg_group_optimizer;
// The detail filter: how specialist a flag may be and still be listed.
const Msg& tier_label(int rank) {
switch (rank) {
case 0: return msg::cfg_tier_basic;
case 1: return msg::cfg_tier_advanced;
default: return msg::cfg_tier_all;
}
}
constexpr int kNumTierChoices = 3;
// Fields owned by dedicated GUI controls; hidden from the generated list.
bool gui_managed(const char* cli_key) {
@@ -234,48 +238,70 @@ bool gui_managed(const char* cli_key) {
bool draw_config_editor(TrainConfig& cfg, const TrainConfig& defaults,
ConfigUIState& st) {
ImGui::SetNextItemWidth(-115);
ImGui::SetNextItemWidth(-250);
ui::InputTextHintBufRaw("##cfgsearch", msg::cfg_search_hint,
st.search, sizeof st.search);
ImGui::SameLine();
ImGui::SetNextItemWidth(130);
// The list is short and its entries are the label, so the combo carries
// no separate caption -- what it filters is explained on hover.
const bool tier_open = ui::BeginComboRaw("##cfgtier", tier_label(st.tier).get());
if (!tier_open) ui::help_on_hover(msg::cfg_tier_help);
if (tier_open) {
for (int i = 0; i < kNumTierChoices; i++)
if (ui::Selectable(tier_label(i), i == st.tier)) st.tier = i;
ImGui::EndCombo();
}
ImGui::SameLine();
ui::Checkbox(msg::cfg_edited_only, &st.modified_only);
ui::help_on_hover(msg::cfg_edited_only_help);
const bool searching = st.search[0] != 0 || st.modified_only;
// Searching reaches past the detail filter: a flag you can name should be
// findable without first working out how specialist it is.
const int max_tier = searching ? kTrainNumTiers - 1
: st.tier >= kNumTierChoices - 1 ? kTrainNumTiers - 1
: st.tier;
auto passes = [&](const char* key, const char* help, bool modified) {
auto passes = [&](const char* key, const char* tier, const char* help,
bool modified) {
if (gui_managed(key)) return false;
if (train_tier_rank(tier) > max_tier) return false;
if (st.modified_only && !modified) return false;
if (st.search[0] && !icontains(key, st.search) && !icontains(help, st.search))
return false;
return true;
};
// Pass 1: per-group visible-field counts (groups are contiguous in the
// field table, so pass 2 can stream group headers).
int vis[5] = {0, 0, 0, 0, 0};
#define SS_COUNT(type, member, default_, group, choices, help) \
if (passes(#member, help, !(cfg.member == defaults.member))) \
vis[group_index(group)]++;
// Pass 1: per-section visible-field counts (sections are contiguous in
// the field table, so pass 2 can stream headings).
int vis[kTrainNumSections] = {0};
#define SS_COUNT(type, member, default_, section, tier, choices, help) \
if (passes(#member, tier, help, !(cfg.member == defaults.member))) \
vis[train_section_index(section)]++;
SS_CONFIG_FIELDS(SS_COUNT)
#undef SS_COUNT
// Pass 2: draw.
bool any_changed = false;
const char* cur_group = "";
bool group_open = false;
#define SS_DRAW(type, member, default_, group, choices, help) \
if (std::strcmp(cur_group, group) != 0) { \
cur_group = group; \
if (vis[group_index(group)] == 0) { \
group_open = false; \
const char* cur_section = "";
bool section_open = false;
#define SS_DRAW(type, member, default_, section, tier, choices, help) \
if (std::strcmp(cur_section, section) != 0) { \
cur_section = section; \
if (vis[train_section_index(section)] == 0) { \
section_open = false; \
} else { \
if (searching) ImGui::SetNextItemOpen(true); \
group_open = ui::CollapsingHeader(group_label(group)); \
section_open = ui::CollapsingHeader(section_label(section)); \
} \
} \
if (group_open && passes(#member, help, !(cfg.member == defaults.member))) \
any_changed |= field_row(#member, cfg.member, defaults.member, choices, help);
if (section_open && \
passes(#member, tier, help, !(cfg.member == defaults.member)) && \
field_row(#member, cfg.member, defaults.member, choices, help)) { \
any_changed = true; \
st.touched.insert(#member); \
}
SS_CONFIG_FIELDS(SS_DRAW)
#undef SS_DRAW
+17 -4
View File
@@ -3,9 +3,12 @@
// ConfigUI -- the "Advanced options" editor. All widgets are expanded from
// the SS_CONFIG_FIELDS X-macro (config/TrainConfig.h), so every one of
// the training config fields is editable, with:
// - grouping by config group (Run / Dataset / Data loading / Model /
// Optimizer), collapsed by default so novices are not overwhelmed
// - a search box filtering by flag name and help text
// - grouping under the field table's `section` headings, collapsed by
// default so novices are not overwhelmed
// - a detail filter over the field table's `tier`, so the default view is
// the ~20 flags a first run needs rather than all 178
// - a search box filtering by flag name and help text, which ignores the
// detail filter: searching finds a flag wherever it is buried
// - the field's help text as a hover tooltip (+ the preset default)
// - modified-from-preset highlighting and right-click "Reset to default"
// - fields with `choices` as dropdowns, std::optional as auto/override
@@ -15,16 +18,26 @@
#include "config/TrainConfig.h"
#include <set>
#include <string>
namespace gui {
struct ConfigUIState {
char search[128] = "";
bool modified_only = false;
int tier = 0; // rank into kTrainTiers: show this specialist and below
// Flags the user edited here, by name. Macro options (--quality and
// friends) leave these alone, so touching a flag by hand takes it out of
// a macro's reach for good -- see train_resolve_macros(). Reset when the
// whole config is, i.e. on a preset change.
std::set<std::string> touched;
};
// Draw the full generated editor. `defaults` is the preset-applied baseline
// used for modified-highlighting and reset. Returns true when any field
// changed this frame.
// changed this frame; the caller re-resolves the macro options when it does.
bool draw_config_editor(TrainConfig& cfg, const TrainConfig& defaults,
ConfigUIState& st);
+38 -22
View File
@@ -77,21 +77,14 @@ std::string preset_help(const std::string& name) {
return t ? t->help->get() : "";
}
// True when two configs parse to the same dataset: every dataparser-group
// field plus the non-dataparser fields load_dataset() consumes.
// True when two configs parse to the same dataset -- every field
// load_dataset() consumes, listed as SS_DATASET_PARSE_FIELDS.
bool parse_settings_equal(const TrainConfig& a, const TrainConfig& b) {
bool eq = true;
#define SS_CMP(type, member, default_, group, choices, help) \
if (!std::strcmp(group, "dataparser")) eq = eq && (a.member == b.member);
SS_CONFIG_FIELDS(SS_CMP)
#define SS_CMP(member) eq = eq && (a.member == b.member);
SS_DATASET_PARSE_FIELDS(SS_CMP)
#undef SS_CMP
return eq && a.data == b.data &&
a.warp_to_pinhole == b.warp_to_pinhole &&
a.warp_spherical_to_pinhole == b.warp_spherical_to_pinhole &&
a.load_depths == b.load_depths &&
a.load_normals == b.load_normals &&
a.relative_scale == b.relative_scale &&
a.auto_scale_poses == b.auto_scale_poses;
return eq;
}
// The file-dialog filter list, from DatasetPrep's one list of containers.
@@ -245,6 +238,7 @@ void GuiApp::apply_preset(const std::string& preset) {
_preset = preset;
_cfg = fresh;
_defaults = fresh;
_cfg_ui.touched.clear();
if (!_cfg.data.empty()) {
_viewport.detach();
_runner.load_dataset(_cfg, _preset);
@@ -2239,6 +2233,13 @@ void GuiApp::draw_train_settings() {
draw_config_editor(_cfg, _defaults, _cfg_ui);
ImGui::EndDisabled();
// The macro options (quality, floater_suppression, ...) fill in the flags
// they stand for, skipping any the user has edited by hand -- so the two
// panels above always show the values the run will actually use. None of
// what they write is a dataset-parsing field, so this cannot make the
// snapshot below think the dataset went stale.
train_resolve_macros(_cfg, _cfg_ui.touched);
if (!parse_settings_equal(parse_before, _cfg)) _parse_dirty = true;
// ---- controls + metrics ----
@@ -2247,12 +2248,16 @@ void GuiApp::draw_train_settings() {
draw_metrics();
}
// Every edit here records itself in _cfg_ui.touched, the same way the
// generated editor does: a flag the user set by hand is off limits to the
// macro options (see train_resolve_macros()).
void GuiApp::draw_basic_options() {
const float w = 170.0f;
// Output location first -- the thing every new user looks for.
ImGui::SetNextItemWidth(w);
ui::InputTextRaw("##outdir", &_cfg.output_dir_prefix);
if (ui::InputTextRaw("##outdir", &_cfg.output_dir_prefix))
_cfg_ui.touched.insert("output_dir_prefix");
ImGui::SameLine();
if (ui::ButtonRaw("...##outdir")) {
_pick = PickAction::OutputPrefix;
@@ -2263,8 +2268,9 @@ void GuiApp::draw_basic_options() {
ui::Text(msg::opt_output_folder);
ui::help_on_hover(msg::opt_output_folder_help);
ImGui::SetNextItemWidth(w);
ui::InputTextWithHint(msg::opt_run_name, msg::opt_run_name_hint,
&_cfg.output_dir_name);
if (ui::InputTextWithHint(msg::opt_run_name, msg::opt_run_name_hint,
&_cfg.output_dir_name))
_cfg_ui.touched.insert("output_dir_name");
ui::help_on_hover(msg::opt_run_name_help);
{
std::string run = _cfg.output_dir_name.empty()
@@ -2276,11 +2282,13 @@ void GuiApp::draw_basic_options() {
ImGui::Spacing();
ImGui::SetNextItemWidth(w);
ui::InputInt(msg::opt_steps, &_cfg.num_iterations);
if (ui::InputInt(msg::opt_steps, &_cfg.num_iterations))
_cfg_ui.touched.insert("num_iterations");
ui::help_on_hover(msg::opt_steps_help);
ImGui::SetNextItemWidth(w);
ui::InputInt(msg::opt_max_splats, &_cfg.cap_max);
if (ui::InputInt(msg::opt_max_splats, &_cfg.cap_max))
_cfg_ui.touched.insert("cap_max");
ui::help_on_hover(msg::opt_max_splats_help);
{
@@ -2289,8 +2297,10 @@ void GuiApp::draw_basic_options() {
static const char* prims[] = {"3dgs", "mip", "3dgut"};
int pi = _cfg.primitive == "mip" ? 1 : _cfg.primitive == "3dgut" ? 2 : 0;
ImGui::SetNextItemWidth(w);
if (ui::ComboRaw(ui::detail::label(msg::opt_primitive), &pi, prims, 3))
if (ui::ComboRaw(ui::detail::label(msg::opt_primitive), &pi, prims, 3)) {
_cfg.primitive = prims[pi];
_cfg_ui.touched.insert("primitive");
}
ui::help_on_hover(msg::opt_primitive_help);
}
@@ -2304,6 +2314,7 @@ void GuiApp::draw_basic_options() {
if (ui::ComboRaw(ui::detail::label(msg::opt_resolution), &ds_idx, items, 4)) {
const float vals[] = {0.0f, 2.0f, 4.0f, 8.0f};
_cfg.rescale_camera_to_fit = vals[ds_idx];
_cfg_ui.touched.insert("rescale_camera_to_fit");
}
}
ui::help_on_hover(msg::opt_resolution_help);
@@ -2312,19 +2323,24 @@ void GuiApp::draw_basic_options() {
int mi = _cfg.apply_loss_for_mask ? 1 : 0;
ImGui::SetNextItemWidth(w);
if (ui::Combo(msg::opt_mask_mode, &mi,
{&msg::opt_mask_mode_ignore, &msg::opt_mask_mode_segment}))
{&msg::opt_mask_mode_ignore, &msg::opt_mask_mode_segment})) {
_cfg.apply_loss_for_mask = mi == 1;
_cfg_ui.touched.insert("apply_loss_for_mask");
}
ui::help_on_hover(msg::opt_mask_mode_help);
}
ImGui::SetNextItemWidth(w);
ui::SliderInt(msg::opt_sh_degree, &_cfg.sh_degree, 0, 4);
if (ui::SliderInt(msg::opt_sh_degree, &_cfg.sh_degree, 0, 4))
_cfg_ui.touched.insert("sh_degree");
ui::help_on_hover(msg::opt_sh_degree_help);
ui::Checkbox(msg::opt_bilateral_grid, &_cfg.use_bilateral_grid);
if (ui::Checkbox(msg::opt_bilateral_grid, &_cfg.use_bilateral_grid))
_cfg_ui.touched.insert("use_bilateral_grid");
ui::help_on_hover(msg::opt_bilateral_grid_help);
ui::Checkbox(msg::opt_ppisp, &_cfg.use_ppisp);
if (ui::Checkbox(msg::opt_ppisp, &_cfg.use_ppisp))
_cfg_ui.touched.insert("use_ppisp");
ui::help_on_hover(msg::opt_ppisp_help);
}
+3 -8
View File
@@ -135,13 +135,8 @@ TrainConfig config_from_json(const fs::path& config_json) {
JsonValue root = json_parse_file(config_json.string());
TrainConfig c;
#define SS_LOAD_FIELD(type, member, default_, group, choices, help) \
{ \
const JsonValue* g = std::strcmp(group, "trainer") == 0 \
? &root : root.find(group); \
const JsonValue* v = g ? g->find(train_json_key(#member)) : nullptr; \
if (v) assign(c.member, *v); \
}
#define SS_LOAD_FIELD(type, member, default_, section, tier, choices, help) \
if (const JsonValue* v = root.find(#member)) assign(c.member, *v);
SS_CONFIG_FIELDS(SS_LOAD_FIELD)
#undef SS_LOAD_FIELD
@@ -178,7 +173,7 @@ TrainConfig build_resume_config(const TrainConfig& cli,
throw std::runtime_error("unknown preset: " + preset);
// Explicit flags win over both.
#define SS_APPLY_EXPLICIT(type, member, default_, group, choices, help) \
#define SS_APPLY_EXPLICIT(type, member, default_, section, tier, choices, help) \
if (explicit_flags.count(#member)) base.member = cli.member;
SS_CONFIG_FIELDS(SS_APPLY_EXPLICIT)
#undef SS_APPLY_EXPLICIT
+483 -341
View File
@@ -8,7 +8,7 @@
// TrainerCore -- with no other edit. The struct is expanded from the same
// table, so a field cannot exist in one and not the other.
//
// Row: X(type, member, default, group, choices, help)
// Row: X(type, member, default, section, tier, choices, help)
//
// type one of the scalar types below, or TrainVec3i / TrainVec3f.
// std::array<T, N> cannot appear here: its comma would split the
@@ -17,9 +17,17 @@
// treats '-' and '_' alike, so --sh-degree sets sh_degree.
// default a constant expression. Vector defaults go through train_v3i() /
// train_v3f() for the same comma reason.
// group the section the flag is listed and nested under. Rows must stay
// contiguous per group: --help and the GUI stream group headers as
// they walk the table rather than sorting first.
// section the heading the flag is listed under, one of kTrainSections.
// Rows must stay contiguous per section: --help and the GUI stream
// headings as they walk the table rather than sorting first.
// Purely presentational -- config.json is flat, so a flag can be
// moved to a different heading without breaking anything on disk.
// tier how much of a specialist the flag is for, one of kTrainTiers:
// "basic" the ~20 flags a first run needs. `--help` and the
// GUI's default view show these and nothing else.
// "advanced" reached for often enough to be worth browsing.
// "expert" warmups, schedules and regularizer internals.
// "stub" parsed but not implemented; hidden unless asked for.
// choices '|'-separated list for string fields; "" is free-form; "none"
// means the empty string is allowed and displays as `none`.
// help one line, shown by --help and as the GUI tooltip. Written for
@@ -32,10 +40,11 @@
// means no "e.g." in the first sentence: it ends it early.
#include <array>
#include <cstring>
#include <limits>
#include <optional>
#include <set>
#include <string>
#include <string_view>
// Vector field types and their makers, so the table stays comma-free.
using TrainVec3i = std::array<int, 3>;
@@ -45,13 +54,32 @@ constexpr TrainVec3f train_v3f(float a, float b, float c) { return {a, b, c}; }
inline constexpr float kTrainInf = std::numeric_limits<float>::infinity();
// The run's config.json keys the fields by flag name, with one exception:
// datamanager's split_batch would collide with model.split_batch, so its flag
// is dm_split_batch while the on-disk key stays split_batch. config.json is a
// read-back format (`spirula mesh` and --resume parse it), so this mapping is
// compatibility, not policy -- a new field never needs an entry here.
constexpr const char* train_json_key(const char* flag) {
return std::string_view(flag) == "dm_split_batch" ? "split_batch" : flag;
// The headings, in the order they are listed. The GUI's labels for them live
// in i18n/catalog/Gui.h (cfg_section_*), which this header must not include.
inline constexpr const char* kTrainSections[] = {
"run", "dataset", "scene", "splats", "detail", "loss",
"geometry", "shape", "correction", "colorspace", "perf", "rates",
};
inline constexpr int kTrainNumSections =
(int)(sizeof(kTrainSections) / sizeof(kTrainSections[0]));
inline int train_section_index(const char* section) {
for (int i = 0; i < kTrainNumSections; i++)
if (!std::strcmp(kTrainSections[i], section)) return i;
return 0;
}
// Least specialist first, so "show me everything up to advanced" is a <=.
inline constexpr const char* kTrainTiers[] = {
"basic", "advanced", "expert", "stub",
};
inline constexpr int kTrainNumTiers =
(int)(sizeof(kTrainTiers) / sizeof(kTrainTiers[0]));
inline int train_tier_rank(const char* tier) {
for (int i = 0; i < kTrainNumTiers; i++)
if (!std::strcmp(kTrainTiers[i], tier)) return i;
return kTrainNumTiers - 1;
}
@@ -61,365 +89,385 @@ constexpr const char* train_json_key(const char* flag) {
#define SS_CONFIG_FIELDS(X) \
\
/* ==== trainer -- run control: output, checkpoints, viewer ==== */ \
X(std::string, data, {}, "trainer", "", \
/* ==== run -- run control: where output goes, how long, checkpoints, viewer ==== */ \
X(std::string, data, {}, "run", "basic", "", \
"Folder holding the dataset to train on. COLMAP, Nerfstudio and Metashape layouts are all recognized.") \
X(std::string, resume, "", "trainer", "none", \
X(std::string, resume, "", "run", "advanced", "none", \
"Continue a previous run instead of starting from scratch. Point this at a run's output folder to pick up its newest checkpoint, or at one specific step-*.ckpt folder. The model and dataset settings come from that run, while run length, save cadence and viewer settings still come from the command line. Only works if the earlier run was saved with save_full_checkpoint turned on.") \
X(std::string, output_dir_prefix, "outputs", "trainer", "", \
X(std::string, output_dir_prefix, "outputs", "run", "basic", "", \
"Folder that each run's output directory is created inside.") \
X(std::string, output_dir_name, "", "trainer", "none", \
X(std::string, output_dir_name, "", "run", "basic", "none", \
"Name of this run's folder inside the output prefix. Leave empty to get a name built from the dataset name and the current time.") \
X(int, steps_per_save, 2000, "trainer", "", \
"How often a checkpoint is written, in steps. Use -1 to save only when training finishes, or 0 to never save. Frequent saves cost disk space and a little time.") \
X(bool, save_only_latest_checkpoint, true, "trainer", "", \
"Keep only the newest checkpoint and delete older ones as training goes. Turn off to keep the whole history, which uses considerably more disk space.") \
X(bool, save_full_checkpoint, false, "trainer", "", \
"Also store everything needed to resume training later, not just the finished splats. Checkpoints get much larger because they carry every splat slot and the optimizer state. Leave off if you only want the exported splat file.") \
X(bool, save_eval_images, false, "trainer", "", \
"Write rendered and reference images for the held-out views when training finishes. Useful for judging quality, at the cost of a little disk space.") \
X(int, num_iterations, 30000, "trainer", "", \
X(int, num_iterations, 30000, "run", "basic", "", \
"How long to train, in steps. More steps often give better visual results with diminishing returns, and a proportionally longer wait.") \
X(int, viewer_port, 7007, "trainer", "", \
X(int, steps_per_save, 2000, "run", "advanced", "", \
"How often a checkpoint is written, in steps. Use -1 to save only when training finishes, or 0 to never save. Frequent saves cost disk space and a little time.") \
X(bool, save_only_latest_checkpoint, true, "run", "advanced", "", \
"Keep only the newest checkpoint and delete older ones as training goes. Turn off to keep the whole history, which uses considerably more disk space.") \
X(bool, save_full_checkpoint, false, "run", "advanced", "", \
"Also store everything needed to resume training later, not just the finished splats. Checkpoints get much larger because they carry every splat slot and the optimizer state. Leave off if you only want the exported splat file.") \
X(bool, save_eval_images, false, "run", "advanced", "", \
"Write rendered and reference images for the held-out views when training finishes. Useful for judging quality, at the cost of a little disk space.") \
X(int, viewer_port, 7007, "run", "advanced", "", \
"Network port the built-in web viewer listens on. Change it if that port is already taken.") \
X(bool, disable_viewer, false, "trainer", "", \
X(bool, disable_viewer, false, "run", "advanced", "", \
"Do not start the web viewer. Frees a little memory and avoids port conflicts when several trainings run at once.") \
X(bool, keep_viewer_alive, true, "trainer", "", \
X(bool, keep_viewer_alive, true, "run", "advanced", "", \
"Keep the program running after training finishes so the result stays open in the viewer. Press Ctrl-C to exit. Has no effect when the viewer is disabled.") \
\
/* ==== dataparser -- where the dataset is and how poses are normalized ==== */ \
X(std::string, data_format, "", "dataparser", "colmap|nerfstudio|metashape|none", \
/* ==== dataset -- which files are read, and which images are held out ==== */ \
X(std::string, data_format, "", "dataset", "basic", "colmap|nerfstudio|metashape|none", \
"Which dataset layout to read. Leave empty to detect it from the folder contents.") \
X(std::string, colmap_recon_dir, "", "dataparser", "none", \
"Which COLMAP reconstruction to read, relative to the dataset folder, such as sparse/0. Leave empty to pick automatically: the reconstruction with the most registered images wins.") \
X(std::string, image_dir, "images", "dataparser", "", \
X(std::string, image_dir, "images", "dataset", "basic", "", \
"Subfolder holding the training images, for COLMAP and Metashape datasets.") \
X(std::string, mask_dir, "masks", "dataparser", "", \
X(std::string, mask_dir, "masks", "dataset", "basic", "", \
"Subfolder holding the image masks, for COLMAP and Metashape datasets. What a mask means is set by apply_loss_for_mask.") \
X(std::string, depth_dir, "depths", "dataparser", "", \
"Subfolder holding depth maps, for COLMAP and Metashape datasets. Only read when load_depths is on.") \
X(std::string, normal_dir, "normals", "dataparser", "", \
"Subfolder holding normal maps, for COLMAP and Metashape datasets. Only read when load_normals is on.") \
X(std::string, metashape_xml, "", "dataparser", "none", \
"Metashape camera export to read. Leave empty to find it automatically inside the dataset folder.") \
X(std::string, metashape_ply, "", "dataparser", "none", \
"Metashape point cloud export used to seed the splats. Leave empty to find it automatically.") \
X(std::string, metashape_psx, "", "dataparser", "none", \
"Metashape project file, used to resolve ambiguity when several images in the project share a file name.") \
X(float, rescale_camera_to_fit, 0.0f, "dataparser", "", \
"Fix a mismatch between image size and the camera parameters stored in the dataset. Set it to the factor the images were shrunk by, such as 2 when training on images_2, or 0 to leave the cameras alone. Auto-detection (-1) is not supported yet.") \
X(std::string, downscale_rounding_mode, "floor", "dataparser", "floor|ceil|round", \
"How image size is rounded when divided by rescale_camera_to_fit. Most image downscalers round, so switch to `round` if a pre-shrunk dataset comes out a pixel off and the render looks slightly shifted.") \
X(std::string, orientation_method, "up", "dataparser", "pca|up|vertical|none|gsplat", \
"How the scene is rotated to stand upright. This only affects how the result is framed for viewing, not the splats themselves. Anything other than `up` is approximated for now.") \
X(std::string, center_method, "poses", "dataparser", "poses|focus|none|gsplat", \
"How the scene's origin is chosen. Like the orientation setting, this affects framing rather than the splats themselves. Anything other than `poses` is approximated for now.") \
X(bool, auto_scale_poses, true, "dataparser", "", \
"Normalize the scene so the cameras fit in a unit-sized box. This keeps learning rates and regularizers meaningful across scenes of very different physical size; turn it off only if the dataset is already scaled the way you want.") \
X(float, outlier_threshold, kTrainInf, "dataparser", "", \
"Discard cameras that sit far outside the rest of the capture. Lowering this rejects more, which helps when a few badly estimated poses stretch the scene and throw its scale off. Leave at infinity to keep every camera.") \
X(std::string, train_frame, "points", "dataparser", "normalized|camera|points", \
"Coordinate frame the splats are trained in. Only `points`, the dataset's own frame, is supported.") \
X(std::string, eval_mode, "all", "dataparser", "fraction|filename|interval|all", \
"How images are split between training and held-out evaluation. `all` trains on every image and reports numbers on views it has already seen. `interval` holds out every Nth image, `fraction` holds out a share of them, and `filename` looks for train or eval in the file names. Holding images out gives honest quality numbers at the cost of a few training views.") \
X(float, train_split_fraction, 0.9f, "dataparser", "", \
"Share of the images used for training when eval_mode is `fraction`; the rest are held out.") \
X(int, eval_interval, 8, "dataparser", "", \
"Hold out every Nth image when eval_mode is `interval`. Larger values keep more images for training.") \
X(float, depth_unit_scale_factor, 0.001f, "dataparser", "", \
"Multiplier that turns the stored depth values into scene units. The default reads them as millimeters.") \
X(float, validation_fraction, 0.0f, "dataparser", "", \
"Share of the training images set aside to watch for overfitting. Training can then stop before quality starts to drop. Early stopping is not active yet: the images are held out but the run always goes to the end.") \
\
/* ==== datamanager -- image caching, masks, warping ==== */ \
X(int, max_batch_per_epoch, 800, "datamanager", "", \
"Target number of steps per pass over the dataset, which decides how many images each step uses. Raising it makes each step lighter and cheaper; lowering it groups more images into a step, which is steadier but slower and needs more memory. Datasets smaller than this simply use one image per step.") \
X(std::string, cache_images, "disk", "datamanager", "cpu|gpu|disk", \
"Where decoded training images are kept between steps. `disk` re-reads them and uses the least memory, `cpu` keeps them in RAM for faster steps. `gpu` is not supported yet.") \
X(bool, load_depths, true, "datamanager", "", \
"Use the dataset's depth maps when they exist. They drive depth supervision; turn off to ignore them.") \
X(bool, load_normals, true, "datamanager", "", \
"Use the dataset's normal maps when they exist. They drive normal supervision; turn off to ignore them.") \
X(float, mask_boundary_offset, 0.0f, "datamanager", "", \
X(bool, apply_loss_for_mask, false, "dataset", "basic", "", \
"Whether masked-out pixels are ignored or trained as empty space. Off ignores them, which is how you hide distractions such as people, cars, or the black area outside a fisheye circle. On trains them as empty, which removes the background and leaves just the subject.") \
X(float, mask_boundary_offset, 0.0f, "dataset", "advanced", "", \
"Grow or shrink masks by this fraction of the image size. Negative values pull the mask edge inward, trimming halos and bad pixels along the boundary; positive values push it outward.") \
X(bool, warp_to_pinhole, false, "datamanager", "", \
X(std::string, depth_dir, "depths", "dataset", "basic", "", \
"Subfolder holding depth maps, for COLMAP and Metashape datasets. Only read when load_depths is on.") \
X(std::string, normal_dir, "normals", "dataset", "basic", "", \
"Subfolder holding normal maps, for COLMAP and Metashape datasets. Only read when load_normals is on.") \
X(bool, load_depths, true, "dataset", "basic", "", \
"Use the dataset's depth maps when they exist. They drive depth supervision; turn off to ignore them.") \
X(bool, load_normals, true, "dataset", "basic", "", \
"Use the dataset's normal maps when they exist. They drive normal supervision; turn off to ignore them.") \
X(float, depth_unit_scale_factor, 0.001f, "dataset", "expert", "", \
"Multiplier that turns the stored depth values into scene units. The default reads them as millimeters.") \
X(std::string, colmap_recon_dir, "", "dataset", "basic", "none", \
"Which COLMAP reconstruction to read, relative to the dataset folder, such as sparse/0. Leave empty to pick automatically: the reconstruction with the most registered images wins.") \
X(std::string, metashape_xml, "", "dataset", "advanced", "none", \
"Metashape camera export to read. Leave empty to find it automatically inside the dataset folder.") \
X(std::string, metashape_ply, "", "dataset", "advanced", "none", \
"Metashape point cloud export used to seed the splats. Leave empty to find it automatically.") \
X(std::string, metashape_psx, "", "dataset", "advanced", "none", \
"Metashape project file, used to resolve ambiguity when several images in the project share a file name.") \
X(float, rescale_camera_to_fit, 0.0f, "dataset", "advanced", "", \
"Fix a mismatch between image size and the camera parameters stored in the dataset. Set it to the factor the images were shrunk by, such as 2 when training on images_2, or 0 to leave the cameras alone. Auto-detection (-1) is not supported yet.") \
X(std::string, downscale_rounding_mode, "floor", "dataset", "advanced", "floor|ceil|round", \
"How image size is rounded when divided by rescale_camera_to_fit. Most image downscalers round, so switch to `round` if a pre-shrunk dataset comes out a pixel off and the render looks slightly shifted.") \
X(std::string, eval_mode, "all", "dataset", "advanced", "fraction|filename|interval|all", \
"How images are split between training and held-out evaluation. `all` trains on every image and reports numbers on views it has already seen. `interval` holds out every Nth image, `fraction` holds out a share of them, and `filename` looks for train or eval in the file names. Holding images out gives honest quality numbers at the cost of a few training views.") \
X(int, eval_interval, 8, "dataset", "advanced", "", \
"Hold out every Nth image when eval_mode is `interval`. Larger values keep more images for training.") \
X(float, train_split_fraction, 0.9f, "dataset", "advanced", "", \
"Share of the images used for training when eval_mode is `fraction`; the rest are held out.") \
X(float, validation_fraction, 0.0f, "dataset", "expert", "", \
"Share of the training images set aside to watch for overfitting. Training can then stop before quality starts to drop. Early stopping is not active yet: the images are held out but the run always goes to the end.") \
X(bool, warp_to_pinhole, false, "dataset", "advanced", "", \
"Split each fisheye image into five ordinary perspective views before training. Often gives better quality and wider compatibility for fisheye and 360 captures, at the cost of more images to process.") \
X(bool, warp_spherical_to_pinhole, true, "datamanager", "", \
X(bool, warp_spherical_to_pinhole, true, "dataset", "advanced", "", \
"Split each 360 panorama into six cube faces before training. Turn this off to train directly on the panorama, which keeps the original pixels but cannot use depth or normal supervision.") \
X(bool, deblur_training_images, false, "datamanager", "", \
X(bool, deblur_training_images, false, "dataset", "stub", "", \
"Sharpen blurry photos with a learned deblurring model before training. Not supported yet.") \
\
/* ==== model -- the splat model, losses, densification, regularizers ==== */ \
X(std::string, primitive, "3dgs", "model", "3dgs|mip|3dgut", \
"Shape used for each splat. `3dgs` is the standard Gaussian most compatible with mainstream viewers, `mip` reduces aliasing when views differ a lot in distance or resolution, and `3dgut` handles wide-angle and distorted lenses more accurately and gives cleaner geometry for meshing.") \
X(int, sh_degree, 3, "model", "", \
"How much the color of a splat may change with viewing angle. Higher shows sharper reflections and shading as the camera moves, at the cost of memory and file size; 0 gives flat, view-independent color. Values of 4 or higher have limited support in mainstream viewers.") \
X(int, sh_degree_warmup_every, 1000, "model", "", \
"How many steps between each step up in view-dependent color detail. Introducing it gradually keeps early training stable and stops reflections from being baked in too early. Small values reach full detail almost immediately.") \
X(std::string, background_mode, "black", "model", "black|noise|sh", \
"What fills pixels no splat covers. `black` is the usual choice, `noise` discourages a semi-transparent haze from forming in empty space, and `sh` learns a skybox so distant background is represented instead of ignored.") \
X(int, background_noise_warmup, 2000, "model", "", \
"How many steps the background noise takes to reach full strength. Only used with the `noise` background.") \
X(float, background_noise_pre_warmup, 0.25f, "model", "", \
"How strong the background noise is at the very start, from 0 to 1. Higher values keep splats from being washed away in the first steps.") \
X(int, background_sh_degree, 4, "model", "", \
"How detailed the learned skybox may be. Higher captures finer sky and distant scenery. Only used with the `sh` background.") \
X(std::optional<float>, relative_scale, std::nullopt, "model", "", \
/* ==== scene -- how the capture is placed, oriented and scaled ==== */ \
X(std::string, orientation_method, "up", "scene", "expert", "pca|up|vertical|none|gsplat", \
"How the scene is rotated to stand upright. This only affects how the result is framed for viewing, not the splats themselves. Anything other than `up` is approximated for now.") \
X(std::string, center_method, "poses", "scene", "expert", "poses|focus|none|gsplat", \
"How the scene's origin is chosen. Like the orientation setting, this affects framing rather than the splats themselves. Anything other than `poses` is approximated for now.") \
X(bool, auto_scale_poses, true, "scene", "expert", "", \
"Normalize the scene so the cameras fit in a unit-sized box. This keeps learning rates and regularizers meaningful across scenes of very different physical size; turn it off only if the dataset is already scaled the way you want.") \
X(float, outlier_threshold, kTrainInf, "scene", "basic", "", \
"Discard cameras that sit far outside the rest of the capture. Lowering this rejects more, which helps when a few badly estimated poses stretch the scene and throw its scale off. Leave at infinity to keep every camera.") \
X(std::optional<float>, relative_scale, std::nullopt, "scene", "expert", "", \
"Multiply the whole scene by this factor before training. Raise it when a large capture comes out too small for detail to resolve. Leave unset to let the optimizer cope with scene scale on its own.") \
X(float, l1_weight, 1.0f, "model", "", \
"Weight of plain per-pixel color error. This is the main term driving color accuracy.") \
X(float, l2_weight, 0.0f, "model", "", \
"Weight of squared per-pixel color error. It punishes large mistakes harder than l1_weight, which makes color settle faster but also chases outliers such as moving objects.") \
X(float, ssim_lambda, 0.2f, "model", "", \
"How much the loss cares about local structure instead of exact pixel color. Higher brings out fine texture and high-frequency detail; lower gives a smoother, less noisy background, which sometimes looks better in outdoor scenes.") \
X(float, l1_weight_y, 0.0f, "model", "", \
"Extra weight on brightness error alone, ignoring hue. Raising it favors luminance detail over color accuracy.") \
X(float, l2_weight_y, 0.0f, "model", "", \
"Extra weight on squared brightness error. Same idea as l1_weight_y, but large brightness mistakes count for much more.") \
X(float, l2_weight_u, 0.0f, "model", "", \
"Extra weight on the blue-versus-yellow color error. Raising it tightens hue accuracy in that direction at the expense of detail elsewhere.") \
X(float, l2_weight_v, 0.0f, "model", "", \
"Extra weight on the red-versus-cyan color error. Raising it tightens hue accuracy in that direction at the expense of detail elsewhere.") \
X(int, num_loss_scales, 0, "model", "", \
"How many progressively smaller copies of each image the loss also compares. Looking at several sizes helps large smooth areas converge on high-resolution datasets instead of only fine detail. Normally left for loss_scale_min_pixels to decide.") \
X(int, loss_scale_min_pixels, 1920, "model", "", \
"Pick the number of loss scales automatically from image size. Images are halved until the shorter side is near this many pixels, so a dataset that mixes resolutions gets the right amount for each image. Set to 0 to use num_loss_scales instead.") \
X(bool, use_camera_optimizer, false, "model", "", \
"Let training nudge the camera poses to absorb small pose errors. Not supported yet.") \
X(bool, packed, true, "model", "", \
"Store projection results compactly. Cuts GPU memory when many images are processed per step, sometimes at a small speed cost.") \
X(bool, use_bvh, false, "model", "", \
"Use a spatial index for splat-tile intersection, which can help when batching many small patches. Not supported yet.") \
X(bool, use_fused_proj_bwd_optim, true, "model", "", \
"Merge the backward pass and the parameter update into one operation. Uses noticeably less memory at large splat counts, for a small speed cost.") \
X(bool, split_batch, true, "model", "", \
"Process a step's images one at a time inside the training step. Cuts peak GPU memory roughly in proportion to how many images each step uses, and gives the same result as processing them together.") \
X(int, quantization_level, 1, "model", "", \
"How compactly splat colors are stored during training. 1 roughly halves the memory spent on view-dependent color with little visible difference; 0 keeps full precision.") \
X(std::string, optimizer_offload, "", "model", "sh|all|none", \
"Move optimizer state to system memory to free up GPU memory. Not supported yet.") \
X(bool, preallocate_splat_tensors, true, "model", "", \
"Reserve memory for the maximum splat count up front. Avoids running out of GPU memory partway through as splats are added, at the cost of holding that memory from the start.") \
X(int, cap_max, 1000000, "model", "", \
"Largest number of splats the scene may grow to. This is the main quality dial: raising it captures more detail and produces a bigger file that renders more slowly. Worth tuning per scene.") \
X(float, min_init_fraction, 0.0f, "model", "", \
"Smallest starting splat count, as a share of cap_max. Raise it when the initial point cloud is sparse, such as synthetic scenes, so training has enough to work with.") \
X(int, refine_every, 100, "model", "", \
"How many steps between rounds of adding and relocating splats. Smaller reacts to missing detail sooner; larger is calmer and slightly cheaper.") \
X(int, refine_start_iter, 500, "model", "", \
"Step at which splats first start being added. Waiting a little lets the initial splats settle before the count starts growing.") \
X(int, refine_stop_num_iter, 5000, "model", "", \
"Stop adding splats this many steps before the end. The remaining steps polish what already exists instead of introducing new splats that never get refined.") \
X(int, refine_stop_iter, 25000, "model", "", \
"Earliest step at which splat growth may stop. Growth ends at whichever comes later, this step or refine_stop_num_iter before the end, so short runs still get to add splats at all.") \
X(float, noise_lr, 80.0f, "model", "", \
"How much random jitter is applied to splat positions early in training. Jitter helps splats escape bad spots and spread into unfilled areas; too much of it blurs detail.") \
X(float, noise_lr_final, 0.8f, "model", "", \
"How much position jitter is left at the end of training. Lower lets detail settle and sharpen over the final steps.") \
X(float, min_opacity, 0.005f, "model", "", \
"Splats fainter than this get recycled into places that need them. Raising it prunes harder and keeps the splat budget on visible surfaces.") \
X(float, growth_factor, 1.05f, "model", "", \
"How fast the splat count grows at each round, as a multiplier. Higher reaches cap_max sooner; lower grows gradually, which tends to place splats more carefully.") \
X(bool, use_revised_densification, true, "model", "", \
"Use the improved rule for deciding where new splats go. It usually recovers missing detail faster; turn off to match the original method.") \
X(std::string, densify_score_mode, "mean", "model", "mean|max|median|geom", \
"How a splat's need-more-detail score builds up over time. `mean` is the balanced default, `max` reacts to a single bad view, `median` ignores the occasional odd view and helps when people or cars move through the scene, and `geom` sits between mean and median.") \
X(float, densify_score_blend_world_grad, 0.0f, "model", "", \
"Balance between adding splats where the image looks wrong and where splats are physically large. Raise toward 1 to spend more splats on big distant structures that image-based scoring tends to starve; 0 uses image error alone.") \
X(std::string, densify_loss_map_mode, "ssim_structure", "model", "none|loss_full|ssim_full|ssim_cs|ssim_structure|edge_aware|robust_edge_aware", \
"What kind of error decides where new splats are added. `ssim_structure` targets mismatched patterns and edges while ignoring brightness differences. `ssim_full`, `ssim_cs` and `loss_full` fold in progressively more of the raw color error. `edge_aware` chases edges in the reference photos whether or not they are already reconstructed well. `robust_edge_aware` does the same but ignores the worst-matching pixels, so moving people and cars do not attract splats. `none` spreads new splats evenly.") \
X(float, densify_robust_edge_aware_quantile, 0.9f, "model", "", \
"How much of the worst-matching image area is ignored when placing splats in `robust_edge_aware` mode. Lower ignores more, which suits captures full of moving distractions; higher keeps more, which suits clean captures where large errors are real detail.") \
X(bool, use_long_axis_split, true, "model", "", \
"Split stretched splats along their long axis when adding detail, rather than splitting them evenly. Gives less blurry distant background in large outdoor scenes.") \
X(TrainVec3f, long_axis_split_opacity_k, train_v3f(0.5f, 0.6f, 8000.0f), "model", "", \
"How much opacity each half keeps when a splat is split. Given as a starting value, a final value, and how many steps to move between them. Higher keeps the halves denser and sharper; lower encourages floaters to fade and relocate to where details are needed.") \
X(float, max_screen_size, 0.3f, "model", "", \
"Shrink splats that cover more than this share of the screen instead of letting them stay huge. Keeps big blobby splats from smearing across the image.") \
X(float, max_screen_size_clip_hardness, 1.5f, "model", "", \
"How firmly the screen-size limit is enforced, from 1 upward. Higher clamps oversized splats decisively; lower eases them down.") \
X(float, max_world_size, kTrainInf, "model", "", \
"Shrink splats bigger than this in world units. Set it when huge floaters show up in the distance in large indoor spaces.") \
X(bool, use_bilateral_grid, true, "model", "", \
"Give each photo its own smooth color correction, absorbing exposure and white balance drift between shots. The splats then keep one consistent color instead of averaging every camera's quirks. Turn off for synthetic or already-consistent datasets.") \
X(TrainVec3i, bilagrid_shape, train_v3i(16, 16, 8), "model", "", \
"How finely the per-photo color correction may vary, as width, height and brightness steps. Finer grids fix more localized shifts and use more VRAM; coarser is safer on flat, low-texture surfaces where a fine grid starts eating real detail.") \
X(std::string, bilagrid_type, "ppisp", "model", "affine|ppisp|loglinear", \
"What the per-photo color correction is allowed to do. `ppisp` adjusts exposure and color gain and shifts hue the least. `affine` is a full color matrix, the most flexible but the most prone to color drift. `loglinear` sits in between.") \
X(bool, use_bilateral_grid_for_geometry, true, "model", "", \
"Apply the same per-photo correction to depth and normal maps. Biased AI-generated maps can then still be used without dragging the geometry off.") \
X(TrainVec3i, bilagrid_shape_geometry, train_v3i(8, 8, 4), "model", "", \
"How finely the depth and normal correction may vary. Same meaning as bilagrid_shape, for geometry rather than color.") \
X(bool, use_adagrad_bilagrid_optim, true, "model", "", \
"Use a steadier update rule for the per-photo color correction. Generally more stable and needs less tuning; turn off to use the scheduled learning rates instead.") \
X(float, bilagrid_tv_loss_weight, 10.0f, "model", "", \
"How smooth the per-photo color correction has to be. Higher keeps corrections gentle and global; lower lets them vary from place to place, which can start absorbing real image detail.") \
X(float, color_shift_reg_weight, 0.0f, "model", "", \
"Keep the per-photo corrections from tinting the result overall. Raise it if the finished splats come out consistently warmer, cooler, darker or brighter than the photos. 0 turns it off, and 0.01 to 1 is the useful range.") \
X(int, color_shift_reg_ema_period, 750, "model", "", \
"How many steps the color-shift check averages over. It should be roughly one pass over the dataset so the average reflects every photo. Ignored when color_shift_reg_weight is 0.") \
X(float, bilagrid_tv_loss_weight_geometry, 10.0f, "model", "", \
"How smooth the depth and normal correction has to be. Higher keeps it gentle and global; lower lets it vary from place to place.") \
X(bool, use_ppisp, true, "model", "", \
"Model per-pixel camera effects such as vignetting, exposure and lens color response. Keeps darkened corners and per-photo exposure shifts out of the splats themselves.") \
X(std::string, ppisp_param_type, "no_crf", "model", "original|rqs|no_crf", \
"Which camera effects get modeled. `no_crf` covers exposure, vignetting and color, then simply clips the result. `original` adds a tone curve on top. `rqs` uses a tone curve that behaves better in dark areas.") \
X(bool, use_adagrad_ppisp_optim, true, "model", "", \
"Use a steadier update rule for the camera-effect model. Generally more stable, needs less tuning, and leads to fewer floaters; turn off to use the scheduled learning rate instead.") \
X(bool, apply_ppisp_before_bilagrid, true, "model", "", \
"Run the camera-effect model before the per-photo color correction rather than after. Only matters when both are on, and decides which of the two absorbs a given color difference.") \
X(float, ppisp_reg_exposure_mean, 1.0f, "model", "", \
"Keep estimated exposures centered around neutral. Stops overall brightness from being counted twice between the splats and the camera model.") \
X(float, ppisp_reg_vig_center, 0.02f, "model", "", \
"Keep estimated vignetting centered near the middle of the image rather than drifting toward a corner.") \
X(float, ppisp_reg_vig_non_pos, 0.01f, "model", "", \
"Keep vignetting darkening the corners rather than brightening them, which is what real lenses do.") \
X(float, ppisp_reg_vig_channel_var, 0.1f, "model", "", \
"Keep vignetting similar across red, green and blue, so image corners do not pick up a color cast.") \
X(float, ppisp_reg_color_mean, 1.0f, "model", "", \
"Keep the per-photo color corrections centered, so no overall tint gets baked into the splats.") \
X(float, ppisp_reg_crf_channel_var, 0.1f, "model", "", \
"Keep the tone curve similar across red, green and blue, so brightness changes do not shift hue.") \
X(bool, image_color_is_linear, false, "model", "", \
"Treat the input images as linear light rather than ordinary display-encoded photos. Set this for renders or captures exported in linear.") \
X(std::string, image_color_gamut, "", "model", "ACES2065-1|ACEScg|Rec.2020|AdobeRGB|DCI-P3|none", \
"Color space the input images were captured in. Leave empty for ordinary sRGB or Rec.709 photos. No tone mapping is applied.") \
X(std::optional<bool>, splat_color_is_linear, std::nullopt, "model", "", \
"Train splat colors in linear light. Leave unset to follow the input images. Linear color holds bright highlights better for HDR work.") \
X(std::string, splat_color_gamut, "", "model", "Rec.709|ACES2065-1|ACEScg|Rec.2020|AdobeRGB|DCI-P3|none", \
"Color space the trained splats are stored in. Leave unset to follow the input images. A wider gamut preserves saturated colors for later grading but needs a viewer that understands it. No tone mapping is applied.") \
X(std::optional<bool>, convert_initial_point_cloud_color, std::nullopt, "model", "", \
"Read the seed point cloud's colors as ordinary sRGB and convert them into the training color space. Turn on when starting colors look wrong in a linear or wide-gamut run.") \
X(std::optional<float>, scale_init, std::nullopt, "model", "", \
X(std::string, train_frame, "points", "scene", "expert", "normalized|camera|points", \
"Coordinate frame the splats are trained in. Only `points`, the dataset's own frame, is supported.") \
\
/* ==== splats -- what a splat is and how the splats start out ==== */ \
X(std::string, primitive, "3dgs", "splats", "basic", "3dgs|mip|3dgut", \
"Shape used for each splat. `3dgs` is the standard Gaussian most compatible with mainstream viewers, `mip` reduces aliasing when views differ a lot in distance or resolution, and `3dgut` handles wide-angle and distorted lenses more accurately and gives cleaner geometry for meshing.") \
X(int, sh_degree, 3, "splats", "basic", "", \
"How much the color of a splat may change with viewing angle. Higher shows sharper reflections and shading as the camera moves, at the cost of memory and file size; 0 gives flat, view-independent color. Values of 4 or higher have limited support in mainstream viewers.") \
X(int, sh_degree_warmup_every, 1000, "splats", "expert", "", \
"How many steps between each step up in view-dependent color detail. Introducing it gradually keeps early training stable and stops reflections from being baked in too early. Small values reach full detail almost immediately.") \
X(std::string, background_mode, "black", "splats", "basic", "black|noise|sh", \
"What fills pixels no splat covers. `black` is the usual choice, `noise` discourages a semi-transparent haze from forming in empty space, and `sh` learns a skybox so distant background is represented instead of ignored.") \
X(int, background_sh_degree, 4, "splats", "basic", "", \
"How detailed the learned skybox may be. Higher captures finer sky and distant scenery. Only used with the `sh` background.") \
X(int, background_noise_warmup, 2000, "splats", "expert", "", \
"How many steps the background noise takes to reach full strength. Only used with the `noise` background.") \
X(float, background_noise_pre_warmup, 0.25f, "splats", "expert", "", \
"How strong the background noise is at the very start, from 0 to 1. Higher values keep splats from being washed away in the first steps.") \
X(std::optional<float>, scale_init, std::nullopt, "splats", "advanced", "", \
"How big each splat starts out. Leave unset to derive it from the point cloud; larger fills space faster but starts blurrier.") \
X(std::optional<float>, opacity_init, std::nullopt, "model", "", \
X(std::optional<float>, opacity_init, std::nullopt, "splats", "advanced", "", \
"How solid each splat starts out. Leave unset to choose automatically; lower makes early training more forgiving, higher locks geometry in sooner.") \
X(bool, suppress_initial_scales, false, "model", "", \
X(bool, suppress_initial_scales, false, "splats", "expert", "", \
"Start splats small where the point cloud is sparse. Keeps them from blooming into large floaters over empty space.") \
X(float, scale_regularization_weight, 0.0f, "model", "", \
"Penalize splats that are far longer in one direction than another, which suppresses long spiky artifacts.") \
X(float, max_gauss_ratio, 10.0f, "model", "", \
"How stretched a splat may get before the spiky-splat penalty applies. Lower forces rounder splats.") \
X(float, depth_distortion_reg, 0.0f, "model", "", \
"Encourage each pixel's depth to come from one surface and discourage floaters. Gives crisper geometry and better meshes; too much flattens fine translucent detail.") \
X(float, normal_distortion_reg, 0.0f, "model", "", \
"The same idea as depth distortion, applied to surface direction. Each pixel settles on one consistent orientation instead of several.") \
X(float, rgb_distortion_reg, 0.0f, "model", "", \
"Encourage each pixel's color to come from one surface rather than blended layers. Helps discourage false transparency.") \
X(int, distortion_reg_warmup, 6000, "model", "", \
"How many steps the distortion penalties take to reach full strength. Ramping in lets coarse structure form before geometry is tightened.") \
X(float, normal_reg_weight, 0.04f, "model", "", \
"Encourage splats to lie flat along the surfaces they represent. Higher gives cleaner geometry and better meshes; too high flattens fine detail.") \
X(int, normal_reg_warmup, 6000, "model", "", \
"How many steps the surface-alignment penalty takes to reach full strength.") \
X(float, alpha_reg_weight, 0.0f, "model", "", \
"Push each pixel's coverage toward fully solid or fully empty instead of half transparent. Clears haze and gives cleaner background cutouts, at the risk of less stable training.") \
X(int, alpha_reg_warmup, 12000, "model", "", \
"How many steps the coverage penalty takes to reach full strength.") \
X(int, reg_warmup_length, 0, "model", "", \
"Hold the depth, normal and coverage penalties off for this many steps. Lets the scene take shape before geometry constraints start pulling on it.") \
X(bool, apply_loss_for_mask, false, "model", "", \
"Whether masked-out pixels are ignored or trained as empty space. Off ignores them, which is how you hide distractions such as people, cars, or the black area outside a fisheye circle. On trains them as empty, which removes the background and leaves just the subject.") \
X(float, alpha_loss_weight, 0.01f, "model", "", \
X(bool, use_camera_optimizer, false, "splats", "stub", "", \
"Let training nudge the camera poses to absorb small pose errors. Not supported yet.") \
\
/* ==== detail -- how many splats there are and where they go ==== */ \
X(std::string, quality, "medium", "detail", "basic", "low|medium|high|ultra", \
"Overall detail level, setting the splat budget and how long training runs. Higher looks better, takes longer and produces a larger file. Whatever you set by hand always wins over this.") \
X(int, cap_max, 1000000, "detail", "basic", "", \
"Largest number of splats the scene may grow to. This is the main quality dial: raising it captures more detail and produces a bigger file that renders more slowly. Worth tuning per scene.") \
X(std::string, distraction_robustness, "off", "detail", "basic", "off|mild|strong", \
"Ignore people, cars and anything else that moves between photos. Splats are no longer spent on them, at the cost of some sensitivity to real detail. Whatever you set by hand always wins over this.") \
X(float, min_init_fraction, 0.0f, "detail", "basic", "", \
"Smallest starting splat count, as a share of cap_max. Raise it when the initial point cloud is sparse, such as synthetic scenes, so training has enough to work with.") \
X(float, growth_factor, 1.05f, "detail", "advanced", "", \
"How fast the splat count grows at each round, as a multiplier. Higher reaches cap_max sooner; lower grows gradually, which tends to place splats more carefully.") \
X(float, min_opacity, 0.005f, "detail", "advanced", "", \
"Splats fainter than this get recycled into places that need them. Raising it prunes harder and keeps the splat budget on visible surfaces.") \
X(int, refine_every, 100, "detail", "advanced", "", \
"How many steps between rounds of adding and relocating splats. Smaller reacts to missing detail sooner; larger is calmer and slightly cheaper.") \
X(int, refine_start_iter, 500, "detail", "expert", "", \
"Step at which splats first start being added. Waiting a little lets the initial splats settle before the count starts growing.") \
X(int, refine_stop_num_iter, 5000, "detail", "advanced", "", \
"Stop adding splats this many steps before the end. The remaining steps polish what already exists instead of introducing new splats that never get refined.") \
X(int, refine_stop_iter, 25000, "detail", "advanced", "", \
"Earliest step at which splat growth may stop. Growth ends at whichever comes later, this step or refine_stop_num_iter before the end, so short runs still get to add splats at all.") \
X(float, noise_lr, 80.0f, "detail", "expert", "", \
"How much random jitter is applied to splat positions early in training. Jitter helps splats escape bad spots and spread into unfilled areas; too much of it blurs detail.") \
X(float, noise_lr_final, 0.8f, "detail", "expert", "", \
"How much position jitter is left at the end of training. Lower lets detail settle and sharpen over the final steps.") \
X(bool, use_revised_densification, true, "detail", "expert", "", \
"Use the improved rule for deciding where new splats go. It usually recovers missing detail faster; turn off to match the original method.") \
X(std::string, densify_score_mode, "mean", "detail", "basic", "mean|max|median|geom", \
"How a splat's need-more-detail score builds up over time. `mean` is the balanced default, `max` reacts to a single bad view, `median` ignores the occasional odd view and helps when people or cars move through the scene, and `geom` sits between mean and median.") \
X(float, densify_score_blend_world_grad, 0.0f, "detail", "advanced", "", \
"Balance between adding splats where the image looks wrong and where splats are physically large. Raise toward 1 to spend more splats on big distant structures that image-based scoring tends to starve; 0 uses image error alone.") \
X(std::string, densify_loss_map_mode, "ssim_structure", "detail", "basic", "none|loss_full|ssim_full|ssim_cs|ssim_structure|edge_aware|robust_edge_aware", \
"What kind of error decides where new splats are added. `ssim_structure` targets mismatched patterns and edges while ignoring brightness differences. `ssim_full`, `ssim_cs` and `loss_full` fold in progressively more of the raw color error. `edge_aware` chases edges in the reference photos whether or not they are already reconstructed well. `robust_edge_aware` does the same but ignores the worst-matching pixels, so moving people and cars do not attract splats. `none` spreads new splats evenly.") \
X(float, densify_robust_edge_aware_quantile, 0.9f, "detail", "basic", "", \
"How much of the worst-matching image area is ignored when placing splats in `robust_edge_aware` mode. Lower ignores more, which suits captures full of moving distractions; higher keeps more, which suits clean captures where large errors are real detail.") \
X(bool, use_long_axis_split, true, "detail", "expert", "", \
"Split stretched splats along their long axis when adding detail, rather than splitting them evenly. Gives less blurry distant background in large outdoor scenes.") \
X(TrainVec3f, long_axis_split_opacity_k, train_v3f(0.5f, 0.6f, 8000.0f), "detail", "basic", "", \
"How much opacity each half keeps when a splat is split. Given as a starting value, a final value, and how many steps to move between them. Higher keeps the halves denser and sharper; lower encourages floaters to fade and relocate to where details are needed.") \
X(float, max_screen_size, 0.3f, "detail", "basic", "", \
"Shrink splats that cover more than this share of the screen instead of letting them stay huge. Keeps big blobby splats from smearing across the image.") \
X(float, max_screen_size_clip_hardness, 1.5f, "detail", "basic", "", \
"How firmly the screen-size limit is enforced, from 1 upward. Higher clamps oversized splats decisively; lower eases them down.") \
X(float, max_world_size, kTrainInf, "detail", "expert", "", \
"Shrink splats bigger than this in world units. Set it when huge floaters show up in the distance in large indoor spaces.") \
\
/* ==== loss -- how the render is compared against the photo ==== */ \
X(float, ssim_lambda, 0.2f, "loss", "basic", "", \
"How much the loss cares about local structure instead of exact pixel color. Higher brings out fine texture and high-frequency detail; lower gives a smoother, less noisy background, which sometimes looks better in outdoor scenes.") \
X(float, l1_weight, 1.0f, "loss", "basic", "", \
"Weight of plain per-pixel color error. This is the main term driving color accuracy.") \
X(float, l2_weight, 0.0f, "loss", "basic", "", \
"Weight of squared per-pixel color error. It punishes large mistakes harder than l1_weight, which makes color settle faster but also chases outliers such as moving objects.") \
X(float, l1_weight_y, 0.0f, "loss", "advanced", "", \
"Extra weight on brightness error alone, ignoring hue. Raising it favors luminance detail over color accuracy.") \
X(float, l2_weight_y, 0.0f, "loss", "advanced", "", \
"Extra weight on squared brightness error. Same idea as l1_weight_y, but large brightness mistakes count for much more.") \
X(float, l2_weight_u, 0.0f, "loss", "advanced", "", \
"Extra weight on the blue-versus-yellow color error. Raising it tightens hue accuracy in that direction at the expense of detail elsewhere.") \
X(float, l2_weight_v, 0.0f, "loss", "advanced", "", \
"Extra weight on the red-versus-cyan color error. Raising it tightens hue accuracy in that direction at the expense of detail elsewhere.") \
X(int, loss_scale_min_pixels, 1920, "loss", "advanced", "", \
"Pick the number of loss scales automatically from image size. Images are halved until the shorter side is near this many pixels, so a dataset that mixes resolutions gets the right amount for each image. Set to 0 to use num_loss_scales instead.") \
X(int, num_loss_scales, 0, "loss", "advanced", "", \
"How many progressively smaller copies of each image the loss also compares. Looking at several sizes helps large smooth areas converge on high-resolution datasets instead of only fine detail. Normally left for loss_scale_min_pixels to decide.") \
X(float, alpha_loss_weight, 0.01f, "loss", "basic", "", \
"How firmly to clear splats out of areas the mask says should be empty. Raise it if background creeps back in.") \
X(float, alpha_loss_weight_under, 0.0f, "model", "", \
X(float, alpha_loss_weight_under, 0.0f, "loss", "basic", "", \
"How firmly to fill in areas the mask says should be solid but the render leaves empty. Raise it if holes appear in the subject.") \
X(float, opacity_reg, 0.01f, "model", "", \
"Gently push splat opacity down so weak splats get recycled where they are needed more. Higher recycles more aggressively.") \
X(float, scale_reg, 0.01f, "model", "", \
"Gently push splat size down. Keeps splats compact so detail stays local; too high leaves large flat areas underfilled.") \
X(float, opacity_decay, 0.0f, "model", "", \
"Fade every splat's opacity slightly each step, freeing weak ones for reuse. An alternative to opacity_reg that acts on all splats equally.") \
X(float, scale_decay, 0.0f, "model", "", \
"Shrink every splat slightly each step. An alternative to scale_reg that acts on all splats equally.") \
X(float, erank_reg, 0.0f, "model", "", \
"Discourage needle-like splats in favor of rounder ones. Reduces spiky artifacts and flicker as the camera moves.") \
X(float, erank_reg_s3, 0.0f, "model", "", \
"Discourage splats from collapsing into flat sheets, keeping some thickness in every direction.") \
X(float, quat_norm_reg, 0.01f, "model", "", \
"Keep splat rotations well formed. Guards against numerical drift and rarely needs changing.") \
X(float, sh_reg, 0.001f, "model", "", \
"Hold view-dependent color in check so it does not absorb shifts the per-photo correction should handle. Higher gives more consistent color from every angle and better results on unseen views; too high flattens genuine reflections.") \
X(float, overexposure_reg, 0.0f, "model", "", \
"Penalize splat colors that fall outside the displayable range. Keeps blown-out highlights from hiding inside the splats, which matters when exporting or meshing.") \
X(int, supervision_warmup, 0, "model", "", \
"Step at which AI-predicted depth and normals start guiding training. Waiting lets photometric detail establish itself first.") \
X(float, depth_supervision_weight, 0.0f, "model", "", \
\
/* ==== geometry -- how crisp the surfaces come out, and depth/normal guidance ==== */ \
X(std::string, floater_suppression, "off", "geometry", "basic", "off|mild|strong", \
"Clean up floating blobs and see-through surfaces. Tightens the depth and colour consistency penalties and holds view-dependent colour back; `strong` gives the crispest geometry but can flatten thin or genuinely translucent detail. Whatever you set by hand always wins over this.") \
X(float, depth_distortion_reg, 0.0f, "geometry", "basic", "", \
"Encourage each pixel's depth to come from one surface and discourage floaters. Gives crisper geometry and better meshes; too much flattens fine translucent detail.") \
X(float, normal_distortion_reg, 0.0f, "geometry", "basic", "", \
"The same idea as depth distortion, applied to surface direction. Each pixel settles on one consistent orientation instead of several.") \
X(float, rgb_distortion_reg, 0.0f, "geometry", "basic", "", \
"Encourage each pixel's color to come from one surface rather than blended layers. Helps discourage false transparency.") \
X(int, distortion_reg_warmup, 6000, "geometry", "basic", "", \
"How many steps the distortion penalties take to reach full strength. Ramping in lets coarse structure form before geometry is tightened.") \
X(float, normal_reg_weight, 0.04f, "geometry", "basic", "", \
"Encourage splats to lie flat along the surfaces they represent. Higher gives cleaner geometry and better meshes; too high flattens fine detail.") \
X(int, normal_reg_warmup, 6000, "geometry", "advanced", "", \
"How many steps the surface-alignment penalty takes to reach full strength.") \
X(float, alpha_reg_weight, 0.0f, "geometry", "basic", "", \
"Push each pixel's coverage toward fully solid or fully empty instead of half transparent. Clears haze and gives cleaner background cutouts, at the risk of less stable training.") \
X(int, alpha_reg_warmup, 12000, "geometry", "advanced", "", \
"How many steps the coverage penalty takes to reach full strength.") \
X(int, reg_warmup_length, 0, "geometry", "advanced", "", \
"Hold the depth, normal and coverage penalties off for this many steps. Lets the scene take shape before geometry constraints start pulling on it.") \
X(float, depth_supervision_weight, 0.0f, "geometry", "advanced", "", \
"How strongly AI-predicted depth guides the geometry. Helps in textureless areas, but a heavily biased prediction can pull quality down.") \
X(bool, input_depth_is_ray_depth, false, "model", "", \
"Whether the supplied depth maps measure distance along the camera ray instead of distance straight ahead. Most AI-predicted depth is the latter, so leave this off; turn it on for very wide fisheye captures where straight-ahead depth is meaningless.") \
X(float, normal_supervision_weight, 0.01f, "model", "", \
X(float, normal_supervision_weight, 0.01f, "geometry", "basic", "", \
"How strongly AI-predicted surface direction guides the geometry. Helps flat surfaces come out flat.") \
X(float, mean_median_depth_weight, 0.0f, "model", "", \
X(bool, input_depth_is_ray_depth, false, "geometry", "basic", "", \
"Whether the supplied depth maps measure distance along the camera ray instead of distance straight ahead. Most AI-predicted depth is the latter, so leave this off; turn it on for very wide fisheye captures where straight-ahead depth is meaningless.") \
X(int, supervision_warmup, 0, "geometry", "expert", "", \
"Step at which AI-predicted depth and normals start guiding training. Waiting lets photometric detail establish itself first.") \
X(float, mean_median_depth_weight, 0.0f, "geometry", "advanced", "", \
"Encourage a pixel's average depth and its most-solid depth to agree, which pulls splats onto a single surface. Useful when the result is destined for a mesh.") \
X(float, median_depth_normal_reg_weight, 0.0f, "model", "", \
X(float, median_depth_normal_reg_weight, 0.0f, "geometry", "expert", "", \
"Encourage surface direction to agree between the average depth and the most-solid depth. Another crispness dial for meshing.") \
X(float, median_normal_supervision_weight, 0.0f, "model", "", \
X(float, median_normal_supervision_weight, 0.0f, "geometry", "expert", "", \
"Match the surface direction at the most-solid depth to AI-predicted normals.") \
X(float, median_render_normal_reg_weight, 0.0f, "model", "", \
X(float, median_render_normal_reg_weight, 0.0f, "geometry", "expert", "", \
"Match the surface direction at the most-solid depth to the rendered normals.") \
X(int, median_warmup, 6000, "model", "", \
X(int, median_warmup, 6000, "geometry", "expert", "", \
"How many steps the four most-solid-depth penalties take to reach full strength.") \
\
/* ==== optimizer -- learning rates and schedules ==== */ \
X(std::optional<int>, max_steps, std::nullopt, "optimizer", "", \
"Length of the learning-rate schedule, in steps. Leave unset to match num_iterations. Set it to keep the rates on their usual curve when training longer or shorter than the schedule was designed for.") \
X(bool, use_scale_agnostic_mean, true, "optimizer", "", \
"Make how fast splats move independent of how large the scene is, so one setting works across datasets. Turn off to have it scale with the scene, matching the original 3DGS behavior.") \
X(bool, use_per_splat_bias_correction, true, "optimizer", "", \
"Give newly created splats a fresh start in the optimizer. They then move at full speed instead of inheriting the momentum of the splat they came from, so new detail sharpens up faster.") \
X(float, means_lr, 0.000128f, "optimizer", "", \
"How fast splats move through space. Higher rearranges geometry quickly but can jitter and blur; lower stays closer to the starting point cloud.") \
X(std::optional<float>, means_lr_final, 1.6e-06f, "optimizer", "", \
"How fast splats move by the end of training. Positions ease to a stop so detail can settle. Set to none to keep the rate constant.") \
X(float, scales_lr, 0.02f, "optimizer", "", \
"How fast splats change size. Higher adapts coverage quickly; lower keeps sizes near where they started.") \
X(std::optional<float>, scales_lr_final, 0.005f, "optimizer", "", \
"How fast splats change size by the end of training. Set to none to keep the rate constant.") \
X(float, quats_lr, 0.0015f, "optimizer", "", \
"How fast splats rotate, which sets how quickly they align themselves to surfaces.") \
X(float, opacities_lr, 0.025f, "optimizer", "", \
"How fast splat transparency changes. Higher clears haze and prunes faint splats sooner; lower gives weak structure more time to prove itself.") \
X(float, features_dc_lr, 0.005f, "optimizer", "", \
"How fast the base color of a splat changes.") \
X(float, features_sh_lr, 0.00025f, "optimizer", "", \
"How fast view-dependent color changes. Kept well below the base color rate so reflections do not run away with the color.") \
X(float, background_dc_lr, 0.0025f, "optimizer", "", \
"How fast the skybox base color changes. Only used with the `sh` background.") \
X(float, background_sh_lr, 0.0005f, "optimizer", "", \
"How fast skybox detail changes. Only used with the `sh` background.") \
X(float, bilagrid_lr, 0.002f, "optimizer", "", \
"How fast the per-photo color correction adapts. Higher tracks exposure changes sooner but can start absorbing real detail. Ignored when use_adagrad_bilagrid_optim is on.") \
X(std::optional<float>, bilagrid_lr_final, 0.0001f, "optimizer", "", \
"How fast the per-photo color correction adapts by the end of training. Set to none to keep the rate constant.") \
X(int, bilagrid_lr_warmup, 1000, "optimizer", "", \
"How many steps the per-photo color correction takes to reach full adaptation speed. Ramping in stops it from claiming color before the splats have any.") \
X(float, bilagrid_depth_lr, 0.002f, "optimizer", "", \
"How fast the per-photo depth correction adapts. Ignored when use_adagrad_bilagrid_optim is on.") \
X(std::optional<float>, bilagrid_depth_lr_final, 0.0001f, "optimizer", "", \
"How fast the per-photo depth correction adapts by the end of training. Set to none to keep the rate constant.") \
X(int, bilagrid_depth_lr_warmup, 2000, "optimizer", "", \
"How many steps the per-photo depth correction takes to reach full adaptation speed.") \
X(float, bilagrid_normal_lr, 0.0005f, "optimizer", "", \
"How fast the per-photo normal correction adapts. Ignored when use_adagrad_bilagrid_optim is on.") \
X(std::optional<float>, bilagrid_normal_lr_final, 4e-05f, "optimizer", "", \
"How fast the per-photo normal correction adapts by the end of training. Set to none to keep the rate constant.") \
X(int, bilagrid_normal_lr_warmup, 2000, "optimizer", "", \
"How many steps the per-photo normal correction takes to reach full adaptation speed.") \
X(float, bilagrid_adagrad_lr, 0.04f, "optimizer", "", \
/* ==== shape -- keeping individual splats compact and well behaved ==== */ \
X(float, opacity_reg, 0.01f, "shape", "basic", "", \
"Gently push splat opacity down so weak splats get recycled where they are needed more. Higher recycles more aggressively.") \
X(float, scale_reg, 0.01f, "shape", "basic", "", \
"Gently push splat size down. Keeps splats compact so detail stays local; too high leaves large flat areas underfilled.") \
X(float, opacity_decay, 0.0f, "shape", "basic", "", \
"Fade every splat's opacity slightly each step, freeing weak ones for reuse. An alternative to opacity_reg that acts on all splats equally.") \
X(float, scale_decay, 0.0f, "shape", "basic", "", \
"Shrink every splat slightly each step. An alternative to scale_reg that acts on all splats equally.") \
X(float, erank_reg, 0.0f, "shape", "basic", "", \
"Discourage needle-like splats in favor of rounder ones. Reduces spiky artifacts and flicker as the camera moves.") \
X(float, erank_reg_s3, 0.0f, "shape", "advanced", "", \
"Discourage splats from collapsing into flat sheets, keeping some thickness in every direction.") \
X(float, scale_regularization_weight, 0.0f, "shape", "advanced", "", \
"Penalize splats that are far longer in one direction than another, which suppresses long spiky artifacts.") \
X(float, max_gauss_ratio, 10.0f, "shape", "advanced", "", \
"How stretched a splat may get before the spiky-splat penalty applies. Lower forces rounder splats.") \
X(float, sh_reg, 0.001f, "shape", "basic", "", \
"Hold view-dependent color in check so it does not absorb shifts the per-photo correction should handle. Higher gives more consistent color from every angle and better results on unseen views; too high flattens genuine reflections.") \
X(float, overexposure_reg, 0.0f, "shape", "advanced", "", \
"Penalize splat colors that fall outside the displayable range. Keeps blown-out highlights from hiding inside the splats, which matters when exporting or meshing.") \
X(float, quat_norm_reg, 0.01f, "shape", "advanced", "", \
"Keep splat rotations well formed. Guards against numerical drift and rarely needs changing.") \
\
/* ==== correction -- per-photo exposure, white balance and lens response ==== */ \
X(bool, use_bilateral_grid, true, "correction", "basic", "", \
"Give each photo its own smooth color correction, absorbing exposure and white balance drift between shots. The splats then keep one consistent color instead of averaging every camera's quirks. Turn off for synthetic or already-consistent datasets.") \
X(std::string, bilagrid_type, "ppisp", "correction", "basic", "affine|ppisp|loglinear", \
"What the per-photo color correction is allowed to do. `ppisp` adjusts exposure and color gain and shifts hue the least. `affine` is a full color matrix, the most flexible but the most prone to color drift. `loglinear` sits in between.") \
X(TrainVec3i, bilagrid_shape, train_v3i(16, 16, 8), "correction", "basic", "", \
"How finely the per-photo color correction may vary, as width, height and brightness steps. Finer grids fix more localized shifts and use more VRAM; coarser is safer on flat, low-texture surfaces where a fine grid starts eating real detail.") \
X(float, bilagrid_tv_loss_weight, 10.0f, "correction", "advanced", "", \
"How smooth the per-photo color correction has to be. Higher keeps corrections gentle and global; lower lets them vary from place to place, which can start absorbing real image detail.") \
X(float, color_shift_reg_weight, 0.0f, "correction", "advanced", "", \
"Keep the per-photo corrections from tinting the result overall. Raise it if the finished splats come out consistently warmer, cooler, darker or brighter than the photos. 0 turns it off, and 0.01 to 1 is the useful range.") \
X(int, color_shift_reg_ema_period, 750, "correction", "expert", "", \
"How many steps the color-shift check averages over. It should be roughly one pass over the dataset so the average reflects every photo. Ignored when color_shift_reg_weight is 0.") \
X(bool, use_bilateral_grid_for_geometry, true, "correction", "advanced", "", \
"Apply the same per-photo correction to depth and normal maps. Biased AI-generated maps can then still be used without dragging the geometry off.") \
X(TrainVec3i, bilagrid_shape_geometry, train_v3i(8, 8, 4), "correction", "advanced", "", \
"How finely the depth and normal correction may vary. Same meaning as bilagrid_shape, for geometry rather than color.") \
X(float, bilagrid_tv_loss_weight_geometry, 10.0f, "correction", "advanced", "", \
"How smooth the depth and normal correction has to be. Higher keeps it gentle and global; lower lets it vary from place to place.") \
X(bool, use_adagrad_bilagrid_optim, true, "correction", "advanced", "", \
"Use a steadier update rule for the per-photo color correction. Generally more stable and needs less tuning; turn off to use the scheduled learning rates instead.") \
X(bool, use_ppisp, true, "correction", "basic", "", \
"Model per-pixel camera effects such as vignetting, exposure and lens color response. Keeps darkened corners and per-photo exposure shifts out of the splats themselves.") \
X(std::string, ppisp_param_type, "no_crf", "correction", "basic", "original|rqs|no_crf", \
"Which camera effects get modeled. `no_crf` covers exposure, vignetting and color, then simply clips the result. `original` adds a tone curve on top. `rqs` uses a tone curve that behaves better in dark areas.") \
X(bool, apply_ppisp_before_bilagrid, true, "correction", "advanced", "", \
"Run the camera-effect model before the per-photo color correction rather than after. Only matters when both are on, and decides which of the two absorbs a given color difference.") \
X(bool, use_adagrad_ppisp_optim, true, "correction", "advanced", "", \
"Use a steadier update rule for the camera-effect model. Generally more stable, needs less tuning, and leads to fewer floaters; turn off to use the scheduled learning rate instead.") \
X(float, ppisp_reg_exposure_mean, 1.0f, "correction", "advanced", "", \
"Keep estimated exposures centered around neutral. Stops overall brightness from being counted twice between the splats and the camera model.") \
X(float, ppisp_reg_color_mean, 1.0f, "correction", "advanced", "", \
"Keep the per-photo color corrections centered, so no overall tint gets baked into the splats.") \
X(float, ppisp_reg_vig_center, 0.02f, "correction", "advanced", "", \
"Keep estimated vignetting centered near the middle of the image rather than drifting toward a corner.") \
X(float, ppisp_reg_vig_non_pos, 0.01f, "correction", "advanced", "", \
"Keep vignetting darkening the corners rather than brightening them, which is what real lenses do.") \
X(float, ppisp_reg_vig_channel_var, 0.1f, "correction", "advanced", "", \
"Keep vignetting similar across red, green and blue, so image corners do not pick up a color cast.") \
X(float, ppisp_reg_crf_channel_var, 0.1f, "correction", "advanced", "", \
"Keep the tone curve similar across red, green and blue, so brightness changes do not shift hue.") \
X(float, bilagrid_adagrad_lr, 0.04f, "correction", "advanced", "", \
"How fast the per-photo color correction adapts when use_adagrad_bilagrid_optim is on. This rate is constant, with no schedule or warmup.") \
X(float, bilagrid_adagrad_depth_lr, 0.04f, "optimizer", "", \
X(float, bilagrid_adagrad_depth_lr, 0.04f, "correction", "expert", "", \
"How fast the per-photo depth correction adapts when use_adagrad_bilagrid_optim is on. This rate is constant, with no schedule or warmup.") \
X(float, bilagrid_adagrad_normal_lr, 0.01f, "optimizer", "", \
X(float, bilagrid_adagrad_normal_lr, 0.01f, "correction", "advanced", "", \
"How fast the per-photo normal correction adapts when use_adagrad_bilagrid_optim is on. This rate is constant, with no schedule or warmup.") \
X(float, ppisp_lr, 0.002f, "optimizer", "", \
"How fast the camera-effect model adapts. Ignored when use_adagrad_ppisp_optim is on.") \
X(std::optional<float>, ppisp_lr_final, 2e-05f, "optimizer", "", \
"How fast the camera-effect model adapts by the end of training. Set to none to keep the rate constant.") \
X(int, ppisp_lr_warmup, 500, "optimizer", "", \
"How many steps the camera-effect model takes to reach full adaptation speed. Ramping in stops it from claiming brightness before the splats have any.") \
X(float, ppisp_adagrad_lr, 0.1f, "optimizer", "", \
X(float, ppisp_adagrad_lr, 0.1f, "correction", "advanced", "", \
"How fast the camera-effect model adapts when use_adagrad_ppisp_optim is on. This rate is constant, with no schedule or warmup.") \
X(float, bilagrid_lr, 0.002f, "correction", "advanced", "", \
"How fast the per-photo color correction adapts. Higher tracks exposure changes sooner but can start absorbing real detail. Ignored when use_adagrad_bilagrid_optim is on.") \
X(std::optional<float>, bilagrid_lr_final, 0.0001f, "correction", "advanced", "", \
"How fast the per-photo color correction adapts by the end of training. Set to none to keep the rate constant.") \
X(int, bilagrid_lr_warmup, 1000, "correction", "expert", "", \
"How many steps the per-photo color correction takes to reach full adaptation speed. Ramping in stops it from claiming color before the splats have any.") \
X(float, bilagrid_depth_lr, 0.002f, "correction", "expert", "", \
"How fast the per-photo depth correction adapts. Ignored when use_adagrad_bilagrid_optim is on.") \
X(std::optional<float>, bilagrid_depth_lr_final, 0.0001f, "correction", "expert", "", \
"How fast the per-photo depth correction adapts by the end of training. Set to none to keep the rate constant.") \
X(int, bilagrid_depth_lr_warmup, 2000, "correction", "expert", "", \
"How many steps the per-photo depth correction takes to reach full adaptation speed.") \
X(float, bilagrid_normal_lr, 0.0005f, "correction", "advanced", "", \
"How fast the per-photo normal correction adapts. Ignored when use_adagrad_bilagrid_optim is on.") \
X(std::optional<float>, bilagrid_normal_lr_final, 4e-05f, "correction", "advanced", "", \
"How fast the per-photo normal correction adapts by the end of training. Set to none to keep the rate constant.") \
X(int, bilagrid_normal_lr_warmup, 2000, "correction", "advanced", "", \
"How many steps the per-photo normal correction takes to reach full adaptation speed.") \
X(float, ppisp_lr, 0.002f, "correction", "advanced", "", \
"How fast the camera-effect model adapts. Ignored when use_adagrad_ppisp_optim is on.") \
X(std::optional<float>, ppisp_lr_final, 2e-05f, "correction", "advanced", "", \
"How fast the camera-effect model adapts by the end of training. Set to none to keep the rate constant.") \
X(int, ppisp_lr_warmup, 500, "correction", "advanced", "", \
"How many steps the camera-effect model takes to reach full adaptation speed. Ramping in stops it from claiming brightness before the splats have any.") \
\
/* ==== colorspace -- linear vs display encoding, and which gamut ==== */ \
X(bool, image_color_is_linear, false, "colorspace", "basic", "", \
"Treat the input images as linear light rather than ordinary display-encoded photos. Set this for renders or captures exported in linear.") \
X(std::string, image_color_gamut, "", "colorspace", "basic", "ACES2065-1|ACEScg|Rec.2020|AdobeRGB|DCI-P3|none", \
"Color space the input images were captured in. Leave empty for ordinary sRGB or Rec.709 photos. No tone mapping is applied.") \
X(std::optional<bool>, splat_color_is_linear, std::nullopt, "colorspace", "basic", "", \
"Train splat colors in linear light. Leave unset to follow the input images. Linear color holds bright highlights better for HDR work.") \
X(std::string, splat_color_gamut, "", "colorspace", "basic", "Rec.709|ACES2065-1|ACEScg|Rec.2020|AdobeRGB|DCI-P3|none", \
"Color space the trained splats are stored in. Leave unset to follow the input images. A wider gamut preserves saturated colors for later grading but needs a viewer that understands it. No tone mapping is applied.") \
X(std::optional<bool>, convert_initial_point_cloud_color, std::nullopt, "colorspace", "basic", "", \
"Read the seed point cloud's colors as ordinary sRGB and convert them into the training color space. Turn on when starting colors look wrong in a linear or wide-gamut run.") \
\
/* ==== perf -- speed and memory; none of these change the result ==== */ \
X(std::string, cache_images, "disk", "perf", "basic", "cpu|gpu|disk", \
"Where decoded training images are kept between steps. `disk` re-reads them and uses the least memory, `cpu` keeps them in RAM for faster steps. `gpu` is not supported yet.") \
X(int, max_batch_per_epoch, 800, "perf", "basic", "", \
"Target number of steps per pass over the dataset, which decides how many images each step uses. Raising it makes each step lighter and cheaper; lowering it groups more images into a step, which is steadier but slower and needs more memory. Datasets smaller than this simply use one image per step.") \
X(bool, split_batch, true, "perf", "advanced", "", \
"Process a step's images one at a time inside the training step. Cuts peak GPU memory roughly in proportion to how many images each step uses, and gives the same result as processing them together.") \
X(bool, use_fused_proj_bwd_optim, true, "perf", "advanced", "", \
"Merge the backward pass and the parameter update into one operation. Uses noticeably less memory at large splat counts, for a small speed cost.") \
X(bool, packed, true, "perf", "advanced", "", \
"Store projection results compactly. Cuts GPU memory when many images are processed per step, sometimes at a small speed cost.") \
X(int, quantization_level, 1, "perf", "advanced", "", \
"How compactly splat colors are stored during training. 1 roughly halves the memory spent on view-dependent color with little visible difference; 0 keeps full precision.") \
X(bool, preallocate_splat_tensors, true, "perf", "expert", "", \
"Reserve memory for the maximum splat count up front. Avoids running out of GPU memory partway through as splats are added, at the cost of holding that memory from the start.") \
X(std::string, optimizer_offload, "", "perf", "stub", "sh|all|none", \
"Move optimizer state to system memory to free up GPU memory. Not supported yet.") \
X(bool, use_bvh, false, "perf", "stub", "", \
"Use a spatial index for splat-tile intersection, which can help when batching many small patches. Not supported yet.") \
\
/* ==== rates -- how fast each splat parameter is allowed to change ==== */ \
X(float, means_lr, 0.000128f, "rates", "basic", "", \
"How fast splats move through space. Higher rearranges geometry quickly but can jitter and blur; lower stays closer to the starting point cloud.") \
X(std::optional<float>, means_lr_final, 1.6e-06f, "rates", "basic", "", \
"How fast splats move by the end of training. Positions ease to a stop so detail can settle. Set to none to keep the rate constant.") \
X(float, scales_lr, 0.02f, "rates", "basic", "", \
"How fast splats change size. Higher adapts coverage quickly; lower keeps sizes near where they started.") \
X(std::optional<float>, scales_lr_final, 0.005f, "rates", "basic", "", \
"How fast splats change size by the end of training. Set to none to keep the rate constant.") \
X(float, quats_lr, 0.0015f, "rates", "basic", "", \
"How fast splats rotate, which sets how quickly they align themselves to surfaces.") \
X(float, opacities_lr, 0.025f, "rates", "basic", "", \
"How fast splat transparency changes. Higher clears haze and prunes faint splats sooner; lower gives weak structure more time to prove itself.") \
X(float, features_dc_lr, 0.005f, "rates", "basic", "", \
"How fast the base color of a splat changes.") \
X(float, features_sh_lr, 0.00025f, "rates", "basic", "", \
"How fast view-dependent color changes. Kept well below the base color rate so reflections do not run away with the color.") \
X(float, background_dc_lr, 0.0025f, "rates", "advanced", "", \
"How fast the skybox base color changes. Only used with the `sh` background.") \
X(float, background_sh_lr, 0.0005f, "rates", "advanced", "", \
"How fast skybox detail changes. Only used with the `sh` background.") \
X(std::optional<int>, max_steps, std::nullopt, "rates", "advanced", "", \
"Length of the learning-rate schedule, in steps. Leave unset to match num_iterations. Set it to keep the rates on their usual curve when training longer or shorter than the schedule was designed for.") \
X(bool, use_scale_agnostic_mean, true, "rates", "expert", "", \
"Make how fast splats move independent of how large the scene is, so one setting works across datasets. Turn off to have it scale with the scene, matching the original 3DGS behavior.") \
X(bool, use_per_splat_bias_correction, true, "rates", "expert", "", \
"Give newly created splats a fresh start in the optimizer. They then move at full speed instead of inheriting the momentum of the splat they came from, so new detail sharpens up faster.") \
/* end */
@@ -428,7 +476,7 @@ constexpr const char* train_json_key(const char* flag) {
// ===========================================================================
struct TrainConfig {
#define SS_DECLARE_FIELD(type, member, default_, group, choices, help) \
#define SS_DECLARE_FIELD(type, member, default_, section, tier, choices, help) \
type member = default_;
SS_CONFIG_FIELDS(SS_DECLARE_FIELD)
#undef SS_DECLARE_FIELD
@@ -440,6 +488,23 @@ struct TrainConfig {
X(data) \
/* end */
// Fields that change what load_dataset() produces: edit one of these in the
// GUI and the parsed dataset it is holding is stale. Spelled out rather than
// derived from `section`, because the two sets are not the same shape (the
// warping and depth/normal flags are listed under other headings) and because
// a heading is free to be reshuffled -- which must not silently change when
// the GUI re-reads a dataset.
#define SS_DATASET_PARSE_FIELDS(X) \
X(data) X(data_format) X(colmap_recon_dir) X(image_dir) X(mask_dir) \
X(depth_dir) X(normal_dir) X(metashape_xml) X(metashape_ply) \
X(metashape_psx) X(rescale_camera_to_fit) 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(eval_interval) X(depth_unit_scale_factor) X(validation_fraction) \
X(warp_to_pinhole) X(warp_spherical_to_pinhole) X(load_depths) \
X(load_normals) X(relative_scale) \
/* end */
// ===========================================================================
// Presets -- named bundles of default overrides, selected as
@@ -482,10 +547,11 @@ inline bool train_apply_preset(TrainConfig& c, const std::string& name) {
c.load_depths = true;
c.load_normals = true;
c.mask_boundary_offset = -0.025f;
c.densify_score_mode = "median";
c.densify_loss_map_mode = "robust_edge_aware";
c.densify_robust_edge_aware_quantile = 0.75f;
c.ssim_lambda = 0.1f;
// Exactly what this preset used to spell out field by field: median
// scoring, robust edge-aware placement, quantile 0.75, ssim 0.1.
// Naming the macro instead means the options editor shows the level
// the preset chose, rather than "off" next to values it moved.
c.distraction_robustness = "strong";
c.rgb_distortion_reg = 0.1f;
c.depth_distortion_reg = 0.01f;
c.sh_degree_warmup_every = 0;
@@ -581,3 +647,79 @@ inline bool train_apply_preset(TrainConfig& c, const std::string& name) {
(void)c;
return false;
}
// ===========================================================================
// Macro options -- one basic flag that moves a handful of specialist ones
// ===========================================================================
// Resolution order is: base defaults -> preset -> macros -> whatever the user
// set. A macro never overwrites a field the user set by hand; that is what
// `explicit_flags` is for (the CLI's `seen` set, the GUI's edited-field set),
// and it is why the flags a macro moves stay ordinary editable flags rather
// than becoming hidden.
//
// A macro sitting at its default writes nothing at all, so a preset that
// already tuned those fields keeps its own values. Naming the macro -- typing
// `--quality medium` -- does write, which is how a preset's tuning is put back
// to stock.
//
// `written`, when given, collects the flags the macros set so that a later
// stage can treat them as explicit as well (--resume does, so that a macro
// passed on the command line still beats the checkpoint's config.json).
//
// Call this once, in the front end, after the preset and the user's flags.
inline void train_resolve_macros(TrainConfig& c,
const std::set<std::string>& explicit_flags,
std::set<std::string>* written = nullptr) {
auto put = [&](const char* key, auto& field, auto value) {
if (explicit_flags.count(key)) return;
field = value;
if (written) written->insert(key);
};
// Splat budget and run length together: raising one without the other
// just spends longer on splats that never get refined.
if (c.quality != "medium" || explicit_flags.count("quality")) {
if (c.quality == "low") {
put("cap_max", c.cap_max, 200000);
put("num_iterations", c.num_iterations, 15000);
} else if (c.quality == "medium") {
put("cap_max", c.cap_max, 1000000);
put("num_iterations", c.num_iterations, 30000);
} else if (c.quality == "high") {
put("cap_max", c.cap_max, 3000000);
put("num_iterations", c.num_iterations, 50000);
} else if (c.quality == "ultra") {
put("cap_max", c.cap_max, 10000000);
put("num_iterations", c.num_iterations, 80000);
}
}
// Score the splat-placement error in a way that a person walking through
// half the photos cannot dominate.
if (c.distraction_robustness != "off") {
put("densify_score_mode", c.densify_score_mode, "median");
put("densify_loss_map_mode", c.densify_loss_map_mode,
"robust_edge_aware");
if (c.distraction_robustness == "mild") {
put("densify_robust_edge_aware_quantile",
c.densify_robust_edge_aware_quantile, 0.9f);
} else {
put("densify_robust_edge_aware_quantile",
c.densify_robust_edge_aware_quantile, 0.75f);
put("ssim_lambda", c.ssim_lambda, 0.1f);
}
}
// Make each pixel commit to one surface, and stop view-dependent colour
// from papering over what is left.
if (c.floater_suppression != "off") {
const bool strong = c.floater_suppression == "strong";
put("depth_distortion_reg", c.depth_distortion_reg,
strong ? 0.03f : 0.01f);
put("rgb_distortion_reg", c.rgb_distortion_reg,
strong ? 0.05f : 0.01f);
put("sh_reg", c.sh_reg, strong ? 0.05f : 0.01f);
}
}
+38 -36
View File
@@ -2200,45 +2200,47 @@ SS_MSG(viewport_render_error,
// Config editor (the "All Options" table)
// ===========================================================================
SS_MSG(cfg_group_run,
EN("Run & Output"), JA("実行と出力"), ZH_HANS("运行与输出"), ZH_HANT("執行與輸出"),
KO("실행과 출력"), DE("Lauf & Ausgabe"), FR("Exécution et sortie"),
ES("Ejecución y salida"), PT("Execução e saída"), IT("Esecuzione e output"),
NL("Run en uitvoer"), RU("Запуск и вывод"), TR("Çalıştırma ve çıktı"));
SS_MSG(cfg_tier_basic,
EN("Basic"), JA("基本"), ZH_HANS("基本"), ZH_HANT("基本"),
KO("기본"), DE("Basis"), FR("Essentiel"), ES("Básico"),
PT("Básico"), IT("Base"), NL("Basis"), RU("Основные"),
TR("Temel"));
SS_MSG(cfg_group_dataparser,
EN("Dataset Parsing"), JA("データセットの解析"), ZH_HANS("数据集解析"),
ZH_HANT("資料集解析"), KO("데이터셋 해석"), DE("Datensatz einlesen"),
FR("Lecture du jeu de données"), ES("Análisis del conjunto"),
PT("Análise do conjunto"), IT("Lettura del set di dati"),
NL("Dataset inlezen"), RU("Разбор набора данных"), TR("Veri kümesi okuma"));
SS_MSG(cfg_tier_advanced,
EN("Advanced"), JA("詳細"), ZH_HANS("进阶"), ZH_HANT("進階"),
KO("고급"), DE("Erweitert"), FR("Avancé"), ES("Avanzado"),
PT("Avançado"), IT("Avanzate"), NL("Geavanceerd"), RU("Дополнительные"),
TR("Gelişmiş"));
SS_MSG(cfg_group_datamanager,
EN("Data Loading"), JA("データの読み込み"), ZH_HANS("数据加载"), ZH_HANT("資料載入"),
KO("데이터 로딩"), DE("Daten laden"), FR("Chargement des données"),
ES("Carga de datos"), PT("Carregamento de dados"), IT("Caricamento dei dati"),
NL("Gegevens laden"), RU("Загрузка данных"), TR("Veri yükleme"));
SS_MSG(cfg_tier_all,
EN("Everything"), JA("すべて"), ZH_HANS("全部"), ZH_HANT("全部"),
KO("전체"), DE("Alles"), FR("Tout"), ES("Todo"),
PT("Tudo"), IT("Tutto"), NL("Alles"), RU("Все"),
TR("Tümü"));
SS_MSG(cfg_group_model,
EN("Model & Losses"), JA("モデルと損失"), ZH_HANS("模型与损失"), ZH_HANT("模型與損失"),
KO("모델과 손실"), DE("Modell & Verluste"), FR("Modèle et pertes"),
ES("Modelo y pérdidas"), PT("Modelo e perdas"), IT("Modello e perdite"),
NL("Model en verliezen"), RU("Модель и потери"), TR("Model ve kayıplar"));
SS_MSG(cfg_group_optimizer,
EN("Optimizer & Learning Rates"),
JA("オプティマイザと学習率"),
ZH_HANS("优化器与学习率"),
ZH_HANT("最佳化器與學習率"),
KO("옵티마이저와 학습률"),
DE("Optimierer & Lernraten"),
FR("Optimiseur et taux d'apprentissage"),
ES("Optimizador y tasas de aprendizaje"),
PT("Otimizador e taxas de aprendizado"),
IT("Ottimizzatore e tassi di apprendimento"),
NL("Optimalisator en leersnelheden"),
RU("Оптимизатор и скорости обучения"),
TR("İyileştirici ve öğrenme oranları"));
SS_MSG(cfg_tier_help,
EN("How specialist an option may be and still be listed. Searching looks "
"through all of them either way."),
JA("一覧に出す設定の細かさです。検索はどの設定でも対象になります。"),
ZH_HANS("列出多少高级选项。搜索始终覆盖全部选项。"),
ZH_HANT("列出多少進階選項。搜尋始終涵蓋全部選項。"),
KO("목록에 나오는 옵션의 전문성 수준입니다. 검색은 언제나 전체를 대상으로 합니다."),
DE("Wie speziell eine Einstellung sein darf, um noch gelistet zu werden. "
"Die Suche geht ohnehin durch alle."),
FR("Jusqu'à quel point une option peut être spécialisée et rester listée. "
"La recherche parcourt toutes les options."),
ES("Hasta qué punto una opción puede ser especializada y seguir listada. "
"La búsqueda las recorre todas."),
PT("Até que ponto uma opção pode ser especializada e continuar listada. "
"A busca percorre todas."),
IT("Quanto può essere specialistica un'opzione e restare in elenco. "
"La ricerca le attraversa tutte."),
NL("Hoe specialistisch een optie mag zijn om nog te worden getoond. "
"Zoeken doorloopt ze hoe dan ook allemaal."),
RU("Насколько узкоспециальным может быть параметр, чтобы попасть в список. "
"Поиск всё равно идёт по всем."),
TR("Bir seçenek listede kalabilmek için ne kadar uzmanlaşmış olabilir. "
"Arama yine de hepsini tarar."));
SS_MSG(cfg_search_hint,
EN("search options (name or description)"),
+142
View File
@@ -351,6 +351,148 @@ inline const PresetText* preset_text(const char* name) {
return nullptr;
}
// ===========================================================================
// Section headings -- the `section` column of config/TrainConfig.h's field
// table, shown by `spirula train --help` and by the GUI's options editor.
// ===========================================================================
SS_MSG(section_run,
EN("Run & Output"), JA("実行と出力"), ZH_HANS("运行与输出"), ZH_HANT("執行與輸出"),
KO("실행과 출력"), DE("Lauf & Ausgabe"), FR("Exécution et sortie"),
ES("Ejecución y salida"), PT("Execução e saída"), IT("Esecuzione e output"),
NL("Run en uitvoer"), RU("Запуск и вывод"), TR("Çalıştırma ve çıktı"));
SS_MSG(section_dataset,
EN("Dataset"), JA("データセット"), ZH_HANS("数据集"), ZH_HANT("資料集"),
KO("데이터셋"), DE("Datensatz"), FR("Jeu de données"),
ES("Conjunto de datos"), PT("Conjunto de dados"), IT("Set di dati"),
NL("Dataset"), RU("Набор данных"), TR("Veri kümesi"));
SS_MSG(section_scene,
EN("Scene Placement"), JA("シーンの位置と向き"), ZH_HANS("场景位置"),
ZH_HANT("場景位置"), KO("장면 위치"), DE("Szenenausrichtung"),
FR("Placement de la scène"), ES("Ubicación de la escena"),
PT("Posicionamento da cena"), IT("Posizione della scena"),
NL("Scèneplaatsing"), RU("Размещение сцены"), TR("Sahne yerleşimi"));
SS_MSG(section_splats,
EN("Splat Model"), JA("スプラットのモデル"), ZH_HANS("泼溅模型"), ZH_HANT("潑濺模型"),
KO("스플랫 모델"), DE("Splat-Modell"), FR("Modèle de splats"),
ES("Modelo de splats"), PT("Modelo de splats"), IT("Modello di splat"),
NL("Splatmodel"), RU("Модель сплатов"), TR("Splat modeli"));
SS_MSG(section_detail,
EN("Detail & Splat Count"),
JA("精細さとスプラット数"),
ZH_HANS("细节与泼溅数"),
ZH_HANT("細節與潑濺數"),
KO("디테일과 스플랫 수"),
DE("Detail & Splat-Anzahl"),
FR("Détail et nombre de splats"),
ES("Detalle y número de splats"),
PT("Detalhe e número de splats"),
IT("Dettaglio e numero di splat"),
NL("Detail en aantal splats"),
RU("Детализация и число сплатов"),
TR("Ayrıntı ve splat sayısı"));
SS_MSG(section_loss,
EN("Image Loss"), JA("画像の損失"), ZH_HANS("图像损失"), ZH_HANT("影像損失"),
KO("이미지 손실"), DE("Bildverlust"), FR("Perte d'image"),
ES("Pérdida de imagen"), PT("Perda de imagem"), IT("Perdita d'immagine"),
NL("Beeldverlies"), RU("Потери по изображению"), TR("Görüntü kaybı"));
SS_MSG(section_geometry,
EN("Geometry & Surfaces"),
JA("ジオメトリと面"),
ZH_HANS("几何与表面"),
ZH_HANT("幾何與表面"),
KO("지오메트리와 표면"),
DE("Geometrie & Oberflächen"),
FR("Géométrie et surfaces"),
ES("Geometría y superficies"),
PT("Geometria e superfícies"),
IT("Geometria e superfici"),
NL("Geometrie en oppervlakken"),
RU("Геометрия и поверхности"),
TR("Geometri ve yüzeyler"));
SS_MSG(section_shape,
EN("Splat Shape"), JA("スプラットの形状"), ZH_HANS("泼溅形状"), ZH_HANT("潑濺形狀"),
KO("스플랫 모양"), DE("Splat-Form"), FR("Forme des splats"),
ES("Forma de los splats"), PT("Forma dos splats"), IT("Forma degli splat"),
NL("Splatvorm"), RU("Форма сплатов"), TR("Splat biçimi"));
SS_MSG(section_correction,
EN("Camera & Color Correction"),
JA("カメラと色の補正"),
ZH_HANS("相机与色彩校正"),
ZH_HANT("相機與色彩校正"),
KO("카메라와 색 보정"),
DE("Kamera- & Farbkorrektur"),
FR("Correction caméra et couleur"),
ES("Corrección de cámara y color"),
PT("Correção de câmera e cor"),
IT("Correzione camera e colore"),
NL("Camera- en kleurcorrectie"),
RU("Коррекция камеры и цвета"),
TR("Kamera ve renk düzeltme"));
SS_MSG(section_colorspace,
EN("Color Space"), JA("色空間"), ZH_HANS("色彩空间"), ZH_HANT("色彩空間"),
KO("색 공간"), DE("Farbraum"), FR("Espace colorimétrique"),
ES("Espacio de color"), PT("Espaço de cor"), IT("Spazio colore"),
NL("Kleurruimte"), RU("Цветовое пространство"), TR("Renk uzayı"));
SS_MSG(section_perf,
EN("Speed & Memory"), JA("速度とメモリ"), ZH_HANS("速度与内存"), ZH_HANT("速度與記憶體"),
KO("속도와 메모리"), DE("Geschwindigkeit & Speicher"), FR("Vitesse et mémoire"),
ES("Velocidad y memoria"), PT("Velocidade e memória"), IT("Velocità e memoria"),
NL("Snelheid en geheugen"), RU("Скорость и память"), TR("Hız ve bellek"));
SS_MSG(section_rates,
EN("Learning Rates"), JA("学習率"), ZH_HANS("学习率"), ZH_HANT("學習率"),
KO("학습률"), DE("Lernraten"), FR("Taux d'apprentissage"),
ES("Tasas de aprendizaje"), PT("Taxas de aprendizado"),
IT("Tassi di apprendimento"), NL("Leersnelheden"),
RU("Скорости обучения"), TR("Öğrenme oranları"));
// ---------------------------------------------------------------------------
// name -> heading. The names are config/TrainConfig.h's kTrainSections, and
// each consumer static_asserts that the two lists are the same length.
// ---------------------------------------------------------------------------
struct SectionText {
const char* name;
const Msg* label;
};
inline constexpr SectionText kSectionText[] = {
{"run", &section_run},
{"dataset", &section_dataset},
{"scene", &section_scene},
{"splats", &section_splats},
{"detail", &section_detail},
{"loss", &section_loss},
{"geometry", &section_geometry},
{"shape", &section_shape},
{"correction", &section_correction},
{"colorspace", &section_colorspace},
{"perf", &section_perf},
{"rates", &section_rates},
};
inline constexpr size_t kNumSectionText =
sizeof(kSectionText) / sizeof(kSectionText[0]);
// Null for a heading with no entry, so a section added to TrainConfig.h
// without text here still lists its flags -- under its bare name.
inline const Msg* section_label(const char* name) {
for (const SectionText& s : kSectionText)
if (std::strcmp(s.name, name) == 0) return s.label;
return nullptr;
}
} // namespace train
} // namespace msg
} // namespace i18n