Skip to content

Commit

Permalink
Improve __repr__ for various objects (#224)
Browse files Browse the repository at this point in the history
Breaking changes:
- `Sim3d.from_matrix` becomes a constructor `Sim3d(matrix)`
- `Camera.{model_id,model_name}` are merged into `Camera.model` (via enum)
- `Camera.params_info` and `Image.{num_points2D,num_points3D}` become read-only properties (previously: functions)

* Improve the quaternion interface
* Improve __repr__ for geometry objects
* Remove summary() and make __repr__ consistent with dataclasses
* Improve __repr__ for maps
* Minor improvements
* Better map naming
* vector_Point2D -> Point2DVector
* Nicer formatting of vectors
* Implicit conversion of array to Rigid3d/Sim3d
* Fix unique_ptr to shared_ptr
* Cleanup of IncrementalTriangulator
* Camera model_id and model_name become model
* More robust detection of function and errors
* Make Image a dataclass
* Use read-only properties so they appear in the summary
* Improvements to Point{2,3}D
  • Loading branch information
sarlinpe authored Jan 10, 2024
1 parent ab04fc8 commit 7a5888c
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 159 deletions.
21 changes: 17 additions & 4 deletions pycolmap/geometry/bindings.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "colmap/geometry/sim3.h"

#include "pycolmap/geometry/homography_matrix.h"
#include "pycolmap/helpers.h"

#include <sstream>

Expand All @@ -19,6 +20,7 @@ void BindGeometry(py::module& m) {
BindHomographyGeometry(m);

py::class_<Eigen::Quaterniond>(m, "Rotation3d")
.def(py::init([]() { return Eigen::Quaterniond::Identity(); }))
.def(py::init<const Eigen::Vector4d&>(), "xyzw"_a)
.def(py::init<const Eigen::Matrix3d&>(), "rotmat"_a)
.def(py::self * Eigen::Quaterniond())
Expand All @@ -30,13 +32,17 @@ void BindGeometry(py::module& m) {
.def("inverse", &Eigen::Quaterniond::inverse)
.def("__repr__", [](const Eigen::Quaterniond& self) {
std::stringstream ss;
ss << "Rotation3d: " << self.coeffs();
ss << "Rotation3d(quat_xyzw=[" << self.coeffs().format(vec_fmt) << "])";
return ss.str();
});
py::implicitly_convertible<py::array, Eigen::Quaterniond>();

py::class_<Rigid3d>(m, "Rigid3d")
.def(py::init<>())
.def(py::init<const Eigen::Quaterniond&, const Eigen::Vector3d&>())
.def(py::init([](const Eigen::Matrix3x4d& matrix) {
return Rigid3d(Eigen::Quaterniond(matrix.leftCols<3>()), matrix.col(3));
}))
.def_readwrite("rotation", &Rigid3d::rotation)
.def_readwrite("translation", &Rigid3d::translation)
.def_property_readonly("matrix", &Rigid3d::ToMatrix)
Expand All @@ -46,15 +52,18 @@ void BindGeometry(py::module& m) {
.def_static("interpolate", &InterpolateCameraPoses)
.def("__repr__", [](const Rigid3d& self) {
std::stringstream ss;
ss << "Rigid3d:\n" << self.ToMatrix();
ss << "Rigid3d("
<< "quat_xyzw=[" << self.rotation.coeffs().format(vec_fmt) << "], "
<< "t=[" << self.translation.format(vec_fmt) << "])";
return ss.str();
});
py::implicitly_convertible<py::array, Rigid3d>();

py::class_<Sim3d>(m, "Sim3d")
.def(py::init<>())
.def(
py::init<double, const Eigen::Quaterniond&, const Eigen::Vector3d&>())
.def_static("from_matrix", &Sim3d::FromMatrix)
.def(py::init(&Sim3d::FromMatrix))
.def_readwrite("scale", &Sim3d::scale)
.def_readwrite("rotation", &Sim3d::rotation)
.def_readwrite("translation", &Sim3d::translation)
Expand All @@ -65,7 +74,11 @@ void BindGeometry(py::module& m) {
.def("inverse", static_cast<Sim3d (*)(const Sim3d&)>(&Inverse))
.def("__repr__", [](const Sim3d& self) {
std::stringstream ss;
ss << "Sim3d:\n" << self.ToMatrix();
ss << "Sim3d("
<< "scale=" << self.scale << ", "
<< "quat_xyzw=[" << self.rotation.coeffs().format(vec_fmt) << "], "
<< "t=[" << self.translation.format(vec_fmt) << "])";
return ss.str();
});
py::implicitly_convertible<py::array, Sim3d>();
}
93 changes: 59 additions & 34 deletions pycolmap/helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <regex>
#include <string>

#include <Eigen/Core>
#include <glog/logging.h>
#include <pybind11/embed.h>
#include <pybind11/eval.h>
Expand All @@ -24,6 +25,11 @@ using namespace colmap;
using namespace pybind11::literals;
namespace py = pybind11;

const Eigen::IOFormat vec_fmt(Eigen::StreamPrecision,
Eigen::DontAlignCols,
", ",
", ");

template <typename T>
inline T pyStringToEnum(const py::enum_<T>& enm, const std::string& value) {
auto values = enm.attr("__members__").template cast<py::dict>();
Expand Down Expand Up @@ -139,45 +145,58 @@ inline py::dict ConvertToDict(const T& self) {
return dict;
}

bool AttributeIsFunction(const std::string& name, const py::object& attribute) {
return (name.find("__") == 0 || name.rfind("__") != std::string::npos ||
py::hasattr(attribute, "__func__") ||
py::hasattr(attribute, "__call__"));
}

template <typename T, typename... options>
inline std::string CreateSummary(const T& self, bool write_type) {
std::stringstream ss;
auto pyself = py::cast(self);
std::string prefix = " ";
const std::string prefix = " ";
bool after_subsummary = false;
ss << pyself.attr("__class__").attr("__name__").template cast<std::string>()
<< ":\n";
<< ":";
for (auto& handle : pyself.attr("__dir__")()) {
std::string attribute = py::str(handle);
auto member = pyself.attr(attribute.c_str());

if (attribute.find("__") != 0 &&
attribute.rfind("__") == std::string::npos &&
!py::hasattr(member, "__func__")) {
if (py::hasattr(member, "summary")) {
std::string summ = member.attr("summary")
.attr("__call__")(write_type)
.template cast<std::string>();
summ = std::regex_replace(summ, std::regex("\n"), "\n" + prefix);
if (!after_subsummary) {
ss << prefix;
}
ss << attribute << ": " << summ;
py::object member;
try {
member = pyself.attr(attribute.c_str());
} catch (const std::exception& e) {
// Some properties are not valid for some uninitialized objects.
continue;
}
if (AttributeIsFunction(attribute, member)) {
continue;
}
ss << "\n";
if (!after_subsummary) {
ss << prefix;
}
ss << attribute;
if (py::hasattr(member, "summary")) {
std::string summ = member.attr("summary")
.attr("__call__")(write_type)
.template cast<std::string>();
summ = std::regex_replace(summ, std::regex("\n"), "\n" + prefix);
ss << ": " << summ;
} else {
if (write_type) {
const std::string type_str =
py::str(py::type::of(member).attr("__name__"));
ss << ": " << type_str;
after_subsummary = true;
} else {
if (!after_subsummary) {
ss << prefix;
}
ss << attribute;
if (write_type) {
ss << ": "
<< py::type::of(member)
.attr("__name__")
.template cast<std::string>();
}
ss << " = " << py::str(member).template cast<std::string>() << "\n";
after_subsummary = false;
}
std::string value = py::str(member);
if (value.length() > 80 && py::hasattr(member, "__len__")) {
const int length = member.attr("__len__")().template cast<int>();
value = StringPrintf(
"%c ... %d elements ... %c", value.front(), length, value.back());
}
ss << " = " << value;
after_subsummary = false;
}
}
return ss.str();
Expand All @@ -188,13 +207,17 @@ void AddDefaultsToDocstrings(py::class_<T, options...> cls) {
auto obj = cls();
for (auto& handle : obj.attr("__dir__")()) {
const std::string attribute = py::str(handle);
const auto member = obj.attr(attribute.c_str());
if (attribute.find("__") == 0 ||
attribute.rfind("__") != std::string::npos ||
py::hasattr(member, "__func__")) {
py::object member;
try {
member = obj.attr(attribute.c_str());
} catch (const std::exception& e) {
// Some properties are not valid for some uninitialized objects.
continue;
}
auto prop = cls.attr(attribute.c_str());
if (AttributeIsFunction(attribute, member)) {
continue;
}
const auto type_name = py::type::of(member).attr("__name__");
const std::string doc =
StringPrintf("%s (%s, default: %s)",
Expand All @@ -209,7 +232,9 @@ template <typename T, typename... options>
inline void MakeDataclass(py::class_<T, options...> cls) {
AddDefaultsToDocstrings(cls);
cls.def("mergedict", &UpdateFromDict);
cls.def("summary", &CreateSummary<T>, "write_type"_a = false);
if (!py::hasattr(cls, "summary")) {
cls.def("summary", &CreateSummary<T>, "write_type"_a = false);
}
cls.def("todict", &ConvertToDict<T>);
cls.def(py::init([cls](py::dict dict) {
auto self = py::object(cls());
Expand Down
84 changes: 33 additions & 51 deletions pycolmap/scene/camera.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
#include "colmap/util/misc.h"
#include "colmap/util/types.h"

#include "pycolmap/helpers.h"
#include "pycolmap/log_exceptions.h"

#include <sstream>

#include <pybind11/eigen.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
Expand All @@ -19,15 +22,17 @@ namespace py = pybind11;
using CameraMap = std::unordered_map<camera_t, Camera>;
PYBIND11_MAKE_OPAQUE(CameraMap);

// TODO: cleanup
std::string PrintCamera(const Camera& camera) {
const bool valid_model = ExistsCameraModelWithId(camera.model_id);
const std::string params_info = valid_model ? camera.ParamsInfo() : "?";
const std::string model_name = valid_model ? camera.ModelName() : "Invalid";
std::stringstream ss;
ss << "<Camera 'camera_id="
ss << "Camera(camera_id="
<< (camera.camera_id != kInvalidCameraId ? std::to_string(camera.camera_id)
: "Invalid")
<< ", model=" << camera.ModelName() << ", width=" << camera.width
<< ", height=" << camera.height << ", num_params=" << camera.params.size()
<< "'>";
<< ", model=" << model_name << ", width=" << camera.width
<< ", height=" << camera.height << ", params=[" << camera.ParamsToString()
<< "] (" << params_info << "))";
return ss.str();
}

Expand All @@ -44,7 +49,21 @@ void BindCamera(py::module& m) {
AddStringToEnumConstructor(PyCameraModelId);
py::implicitly_convertible<int, CameraModelId>();

py::bind_map<CameraMap>(m, "MapCameraIdCamera");
py::bind_map<CameraMap>(m, "MapCameraIdToCamera")
.def("__repr__", [](const CameraMap& self) {
std::stringstream ss;
ss << "{";
bool is_first = true;
for (const auto& pair : self) {
if (!is_first) {
ss << ",\n ";
}
is_first = false;
ss << pair.first << ": " << PrintCamera(pair.second);
}
ss << "}";
return ss.str();
});

py::class_<Camera, std::shared_ptr<Camera>> PyCamera(m, "Camera");
PyCamera.def(py::init<>())
Expand All @@ -57,14 +76,7 @@ void BindCamera(py::module& m) {
"height"_a)
.def_readwrite(
"camera_id", &Camera::camera_id, "Unique identifier of the camera.")
.def_readwrite("model_id", &Camera::model_id, "Camera model ID.")
.def_property(
"model_name",
&Camera::ModelName,
[](Camera& self, std::string model_name) {
self.model_id = CameraModelNameToId(model_name);
},
"Camera model name (connected to model_id).")
.def_readwrite("model", &Camera::model_id, "Camera model.")
.def_readwrite("width", &Camera::width, "Width of camera sensor.")
.def_readwrite("height", &Camera::height, "Height of camera sensor.")
.def("mean_focal_length", &Camera::MeanFocalLength)
Expand Down Expand Up @@ -93,10 +105,11 @@ void BindCamera(py::module& m) {
.def("calibration_matrix",
&Camera::CalibrationMatrix,
"Compute calibration matrix from params.")
.def("params_info",
&Camera::ParamsInfo,
"Get human-readable information about the parameter vector "
"ordering.")
.def_property_readonly(
"params_info",
&Camera::ParamsInfo,
"Get human-readable information about the parameter vector "
"ordering.")
.def_property(
"params",
[](Camera& self) {
Expand Down Expand Up @@ -184,37 +197,6 @@ void BindCamera(py::module& m) {
.def("__copy__", [](const Camera& self) { return Camera(self); })
.def("__deepcopy__",
[](const Camera& self, py::dict) { return Camera(self); })
.def("__repr__", [](const Camera& self) { return PrintCamera(self); })
.def("summary", [](const Camera& self) {
std::stringstream ss;
ss << "Camera:\n\tcamera_id="
<< (self.camera_id != kInvalidCameraId
? std::to_string(self.camera_id)
: "Invalid")
<< "\n\tmodel = " << self.ModelName() << "\n\twidth = " << self.width
<< "\n\theight = " << self.height
<< "\n\tnum_params = " << self.params.size()
<< "\n\tparams_info = " << self.ParamsInfo()
<< "\n\tparams = " << self.ParamsToString();
return ss.str();
});
PyCamera.def(py::init([PyCamera](py::dict dict) {
auto self = py::object(PyCamera());
for (auto& it : dict) {
auto key_str = it.first.cast<std::string>();
if ((key_str == "model") || (key_str == "model_name")) {
self.attr("model_id") = it.second; // Implicit conversion.
} else {
self.attr(it.first) = it.second;
}
}
return self.cast<Camera>();
}),
"dict"_a);
PyCamera.def(py::init([PyCamera](py::kwargs kwargs) {
py::dict dict = kwargs.cast<py::dict>();
return PyCamera(dict).cast<Camera>();
}));

py::implicitly_convertible<py::dict, Camera>();
.def("__repr__", &PrintCamera);
MakeDataclass(PyCamera);
}
8 changes: 4 additions & 4 deletions pycolmap/scene/correspondence_graph.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ void BindCorrespondenceGraph(py::module& m) {
return CorrespondenceGraph::Correspondence(self);
})
.def("__repr__", [](const CorrespondenceGraph::Correspondence& self) {
return "<Correspondence 'image_id=" + std::to_string(self.image_id) +
",point2D_idx=" + std::to_string(self.point2D_idx) + "'>";
return "Correspondence(image_id=" + std::to_string(self.image_id) +
", point2D_idx=" + std::to_string(self.point2D_idx) + ")";
});

py::class_<CorrespondenceGraph, std::shared_ptr<CorrespondenceGraph>>(
Expand Down Expand Up @@ -100,8 +100,8 @@ void BindCorrespondenceGraph(py::module& m) {
})
.def("__repr__", [](const CorrespondenceGraph& self) {
std::stringstream ss;
ss << "<CorrespondenceGraph 'num_images=" << self.NumImages()
<< ", num_image_pairs=" << self.NumImagePairs() << "'>";
ss << "CorrespondenceGraph(num_images=" << self.NumImages()
<< ", num_image_pairs=" << self.NumImagePairs() << ")";
return ss.str();
});
}
Loading

0 comments on commit 7a5888c

Please sign in to comment.