mirror of
https://github.com/harry7557558/spirula-studio.git
synced 2026-10-02 02:44:54 +08:00
fix outliers and missing images in colmap parser
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// Host-only check of camhost::frustum_display_size: the ball average against
|
||||
// quadrature, the ~1% screen budget it promises, 1/sqrt(N) scaling, unit
|
||||
// invariance, bracket merging and the degenerate fallbacks.
|
||||
// invariance, bracket merging, stray cameras and the degenerate fallbacks.
|
||||
// No GPU. Exit code 0 = every check passed.
|
||||
|
||||
#include "data/FrustumSize.h"
|
||||
@@ -163,6 +163,21 @@ void test_fallbacks() {
|
||||
CHECK(nan_cam.size() == s_clean, "a NaN position changed the size");
|
||||
}
|
||||
|
||||
// The Stategallery RealityScan export: 649 cameras within ~16 units and a
|
||||
// handful registered 1000-13800 units out.
|
||||
void test_strays() {
|
||||
Cloud c = fibonacci_sphere(600, 10.0);
|
||||
const double s_clean = c.size();
|
||||
for (double d : {1157.0, 2056.0, 2443.0, 7487.0, 13781.0}) c.add(d, 0.3 * d, 0);
|
||||
CHECK(std::fabs(c.size() - s_clean) < 1e-9 * s_clean,
|
||||
"stray cameras moved the size %.4f -> %.4f", s_clean, c.size());
|
||||
Cloud walk;
|
||||
for (int i = 0; i < 100; i++) walk.add(i, 0, 0);
|
||||
const double s_walk = walk.size();
|
||||
walk.add(150, 0, 0);
|
||||
CHECK(walk.size() != s_walk, "a camera 1.5 walk-lengths on was dropped as a stray");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
@@ -171,6 +186,7 @@ int main() {
|
||||
test_scaling();
|
||||
test_bracket_merge();
|
||||
test_fallbacks();
|
||||
test_strays();
|
||||
if (g_fail) {
|
||||
std::fprintf(stderr, "frustum_size_test: %d check(s) failed\n", g_fail);
|
||||
return 1;
|
||||
|
||||
@@ -360,6 +360,11 @@ PostSplitCameras bake_post_split(const ParsedDataset& ds,
|
||||
// ===========================================================================
|
||||
namespace dsparse {
|
||||
|
||||
// Stray cameras past this many median distances (a failed registration
|
||||
// 1700x out on a RealityScan export) are kept but set no scale. Must match
|
||||
// kStrayOverMedian in data/FrustumSize.h.
|
||||
constexpr float kStrayCameraThreshold = 20.0f;
|
||||
|
||||
// T_n_from_camera = scale * [R_align | -R_align @ center] (row-major 4x4)
|
||||
// over c2w [N,3,4], orient="up" / center="poses"; returns scale_factor. The
|
||||
// viewer remap is inv(that @ applied); `R_out` is R_align alone.
|
||||
|
||||
@@ -28,6 +28,10 @@ constexpr double kShellInner = 0.5, kShellOuter = 2.0;
|
||||
constexpr double kMergeTolerance = 0.01;
|
||||
constexpr double kMaxSizeOverRadius = 0.15; // a handful of cameras must not become billboards
|
||||
constexpr double kFallbackSize = 0.2; // no spread to measure: one distinct position
|
||||
// A camera further than this many median distances out is a failed
|
||||
// registration; kept, it sizes every frustum to itself. Must match
|
||||
// dsparse::kStrayCameraThreshold (data/DatasetParser.h).
|
||||
constexpr double kStrayOverMedian = 20.0;
|
||||
|
||||
// Mean of 1/|c - e|^2 over eyes e uniform in a ball of radius rho, for a
|
||||
// camera c at distance r from the ball's centre. Finite everywhere.
|
||||
@@ -58,6 +62,26 @@ inline P3 centroid(const std::vector<P3>& p) {
|
||||
return c;
|
||||
}
|
||||
|
||||
inline std::vector<P3> drop_strays(std::vector<P3> p) {
|
||||
if (p.size() < 3) return p;
|
||||
P3 m;
|
||||
std::vector<double> v(p.size());
|
||||
for (int k = 0; k < 3; k++) {
|
||||
for (size_t i = 0; i < p.size(); i++) v[i] = p[i][k];
|
||||
std::nth_element(v.begin(), v.begin() + v.size() / 2, v.end());
|
||||
m[k] = v[v.size() / 2];
|
||||
}
|
||||
for (size_t i = 0; i < p.size(); i++) v[i] = dist2(p[i], m);
|
||||
std::vector<double> d = v;
|
||||
std::nth_element(d.begin(), d.begin() + d.size() / 2, d.end());
|
||||
const double lim = kStrayOverMedian * kStrayOverMedian * d[d.size() / 2];
|
||||
if (!(lim > 0.0)) return p;
|
||||
std::vector<P3> kept;
|
||||
for (size_t i = 0; i < p.size(); i++)
|
||||
if (v[i] <= lim) kept.push_back(p[i]);
|
||||
return kept;
|
||||
}
|
||||
|
||||
inline double max_radius(const std::vector<P3>& p, const P3& c) {
|
||||
double r2 = 0.0;
|
||||
for (const P3& q : p) r2 = std::fmax(r2, dist2(q, c));
|
||||
@@ -105,6 +129,7 @@ inline double frustum_display_size(const float* c2w, int64_t n) {
|
||||
P3 q{c2w[i * 12 + 3], c2w[i * 12 + 7], c2w[i * 12 + 11]};
|
||||
if (std::isfinite(q[0]) && std::isfinite(q[1]) && std::isfinite(q[2])) pos.push_back(q);
|
||||
}
|
||||
pos = drop_strays(std::move(pos));
|
||||
if (pos.empty()) return kFallbackSize;
|
||||
double radius = max_radius(pos, centroid(pos));
|
||||
if (!(radius > 0.0)) return kFallbackSize;
|
||||
|
||||
@@ -752,10 +752,11 @@ ParsedDataset parse_colmap_dataset(const std::string& dataset_dir,
|
||||
std::to_string(id) + " has " +
|
||||
std::to_string(cam.params.size()) +
|
||||
" params, expected 2 (w, h)");
|
||||
// COLMAP stores (w, h) as *metadata* params, so they are the camera's
|
||||
// own dimensions by construction. A mismatch means a corrupt or
|
||||
// hand-edited model, and the two would disagree about the projection.
|
||||
if (cam.params[0] != (double)cam.width || cam.params[1] != (double)cam.height)
|
||||
// COLMAP stores (w, h) as *metadata* params, so a mismatch means a
|
||||
// corrupt or hand-edited model. Half a pixel of slack: v2026.9.24's
|
||||
// SfM wrote 2*pi*(w/2pi), which is off by an ulp.
|
||||
if (std::abs(cam.params[0] - (double)cam.width) > 0.5 ||
|
||||
std::abs(cam.params[1] - (double)cam.height) > 0.5)
|
||||
throw std::runtime_error(
|
||||
"ColmapParser: EQUIRECTANGULAR camera " + std::to_string(id) +
|
||||
" params (" + std::to_string((int64_t)cam.params[0]) + ", " +
|
||||
@@ -782,16 +783,32 @@ ParsedDataset parse_colmap_dataset(const std::string& dataset_dir,
|
||||
struct Frame { const ColmapImage* im; std::string path, sort_key, aux_name; };
|
||||
std::vector<Frame> frames;
|
||||
frames.reserve(images.size());
|
||||
std::vector<std::string> missing;
|
||||
for (const auto& [id, im] : images) {
|
||||
std::error_code ec;
|
||||
fs::path path = (fs::path(image_dir) / im.name).lexically_normal();
|
||||
const fs::path in_dataset = (fs::path(dataset_dir) / im.name).lexically_normal();
|
||||
if (!fs::exists(path, ec) && fs::exists(in_dataset, ec)) path = in_dataset;
|
||||
if (cfg.require_image_files && !fs::exists(path, ec)) {
|
||||
missing.push_back(path.string());
|
||||
continue;
|
||||
}
|
||||
std::string aux_name = dsparse::relative_under(path.string(), image_dir);
|
||||
if (aux_name.empty())
|
||||
aux_name = dsparse::relative_under(path.string(), dataset_dir);
|
||||
frames.push_back({&im, path.string(), path.generic_string(), std::move(aux_name)});
|
||||
}
|
||||
// Exporters list images they did not write (RealityScan leaves the ones
|
||||
// it failed to align, with NaN observations), so skip those. None found
|
||||
// is a wrong image dir, not a partial export.
|
||||
if (frames.empty() && !missing.empty())
|
||||
throw std::runtime_error("ColmapParser: " + missing.front() +
|
||||
" does not exist (set --image-dir if needed)");
|
||||
std::sort(missing.begin(), missing.end());
|
||||
for (const std::string& m : missing)
|
||||
std::printf("%s %s\n", spirula::i18n::msg::data::word_warning.get(),
|
||||
spirula::i18n::format(spirula::i18n::msg::data::image_missing,
|
||||
{m}).c_str());
|
||||
std::sort(frames.begin(), frames.end(),
|
||||
[](const Frame& a, const Frame& b) { return a.sort_key < b.sort_key; });
|
||||
|
||||
@@ -912,9 +929,6 @@ ParsedDataset parse_colmap_dataset(const std::string& dataset_dir,
|
||||
std::to_string(im.camera_id));
|
||||
const ColmapCamera& cam = cam_it->second;
|
||||
|
||||
if (cfg.require_image_files && !fs::exists(frames[i].path))
|
||||
throw std::runtime_error("ColmapParser: " + frames[i].path +
|
||||
" does not exist (set --image-dir if needed)");
|
||||
ds.image_filenames.push_back(frames[i].path);
|
||||
|
||||
const int turns =
|
||||
|
||||
@@ -64,18 +64,24 @@ double compute_normalized_transform(const double* c2w, int64_t n,
|
||||
R_out[0] = R_out[4] = R_out[8] = 1.0;
|
||||
}
|
||||
if (n <= 0) return 1.0;
|
||||
std::vector<double> pos(n * 3);
|
||||
for (int64_t i = 0; i < n; i++)
|
||||
for (int r = 0; r < 3; r++) pos[i*3 + r] = c2w[i*12 + r*4 + 3];
|
||||
const std::vector<char> inlier = outlier_keep_mask(pos, n, kStrayCameraThreshold);
|
||||
double up[3] = {0, 0, 0}, center[3] = {0, 0, 0};
|
||||
int64_t n_inlier = 0;
|
||||
for (int64_t i = 0; i < n; i++) {
|
||||
double u[3] = {0, 1, 0};
|
||||
if (exif_orientation) exif_up_gl(exif_orientation[i], u);
|
||||
for (int r = 0; r < 3; r++) {
|
||||
for (int r = 0; r < 3; r++)
|
||||
for (int k = 0; k < 3; k++) up[r] += c2w[i*12 + r*4 + k] * u[k];
|
||||
center[r] += c2w[i*12 + r*4 + 3];
|
||||
}
|
||||
if (!inlier[i]) continue;
|
||||
n_inlier++;
|
||||
for (int r = 0; r < 3; r++) center[r] += pos[i*3 + r];
|
||||
}
|
||||
double un = std::sqrt(up[0]*up[0] + up[1]*up[1] + up[2]*up[2]);
|
||||
for (auto& u : up) u /= std::max(un, 1e-12);
|
||||
for (auto& c : center) c /= (double)n;
|
||||
for (auto& c : center) c /= (double)n_inlier;
|
||||
|
||||
double axis[3] = {up[1], -up[0], 0.0}; // up x z
|
||||
double s = std::sqrt(axis[0]*axis[0] + axis[1]*axis[1]);
|
||||
@@ -98,6 +104,7 @@ double compute_normalized_transform(const double* c2w, int64_t n,
|
||||
|
||||
double max_abs = 0.0;
|
||||
for (int64_t i = 0; i < n; i++) {
|
||||
if (!inlier[i]) continue;
|
||||
double d[3];
|
||||
for (int r = 0; r < 3; r++) d[r] = c2w[i*12 + r*4 + 3] - center[r];
|
||||
for (int r = 0; r < 3; r++)
|
||||
@@ -521,7 +528,9 @@ std::vector<char> outlier_keep_mask(const std::vector<double>& pos,
|
||||
double dx = pos[i*3] - y[0], dy = pos[i*3+1] - y[1], dz = pos[i*3+2] - y[2];
|
||||
dist[i] = std::sqrt(dx*dx + dy*dy + dz*dz);
|
||||
}
|
||||
double mad = median_of(dist);
|
||||
std::vector<double> sorted = dist; // median_of reorders its argument
|
||||
double mad = median_of(sorted);
|
||||
if (!(mad > 0.0)) return keep; // most cameras at one spot: no spread to judge by
|
||||
for (int64_t i = 0; i < n; i++)
|
||||
keep[i] = dist[i] <= (double)threshold * mad;
|
||||
return keep;
|
||||
|
||||
@@ -451,7 +451,8 @@ inline void packColmap(const Camera& c, double* d) {
|
||||
d[0] = c.fx; d[1] = c.fy; d[2] = c.cx; d[3] = c.cy;
|
||||
d[4] = c.k1; d[5] = c.k2; d[6] = c.p1; d[7] = c.p2;
|
||||
d[8] = c.k3; d[9] = c.k4; d[10] = c.sx1; d[11] = c.sy1; break;
|
||||
case CamModel::Equirect: d[0] = 2.0 * M_PI * c.fx; d[1] = M_PI * c.fy; break;
|
||||
// Not 2*pi*fx: that round trip misses the integer width by an ulp.
|
||||
case CamModel::Equirect: d[0] = c.width; d[1] = c.height; break;
|
||||
}
|
||||
}
|
||||
// COLMAP layout -> fields.
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Reference in New Issue
Block a user