diff --git a/cmake/FindDependencies.cmake b/cmake/FindDependencies.cmake index 539a7285..75a05875 100644 --- a/cmake/FindDependencies.cmake +++ b/cmake/FindDependencies.cmake @@ -56,7 +56,7 @@ message(STATUS "Configuring PoseLib... done") # COLMAP FetchContent_Declare(COLMAP GIT_REPOSITORY https://github.com/colmap/colmap.git - GIT_TAG 63b2cc000de32dc697f45ef1576dec7e67abddbc + GIT_TAG c9097b031ee00da22609c7ac4b3f6b45b4178de2 EXCLUDE_FROM_ALL ) message(STATUS "Configuring COLMAP...") diff --git a/limap/base/CMakeLists.txt b/limap/base/CMakeLists.txt index afac5f8a..78e28ee4 100644 --- a/limap/base/CMakeLists.txt +++ b/limap/base/CMakeLists.txt @@ -4,7 +4,6 @@ LIMAP_ADD_SOURCES( graph.h graph.cc camera.h camera.cc camera_models.h - transforms.h transforms.cc camera_view.h camera_view.cc image_collection.h image_collection.cc pose.h pose.cc diff --git a/limap/base/align.py b/limap/base/align.py index 59dbb184..d79f00c3 100644 --- a/limap/base/align.py +++ b/limap/base/align.py @@ -60,7 +60,8 @@ def align_imagecols_umeyama(imagecols_src, imagecols_dst): xyz_src = np.array(imagecols_src.get_locations()).transpose() xyz_dst = np.array(imagecols_dst.get_locations()).transpose() r, t, c = umeyama_alignment(xyz_src, xyz_dst, with_scale=True) - transform = _base.SimilarityTransform3(r, t, c) + matrix = np.concatenate([c * r, t[:, None]], 1) + transform = _base.pycolmap.Sim3d(matrix) imagecols_aligned = imagecols_src.apply_similarity_transform(transform) return transform, imagecols_aligned @@ -142,7 +143,8 @@ def read_trans(fname): scale = np.linalg.norm(transform[:, 0]) R = transform[:3, :3] / scale t = transform[:3, 3] - transform = _base.SimilarityTransform3(R, t, scale) + matrix = np.concatenate([scale * R, t[:, None]], 1) + transform = _base.pycolmap.Sim3d(matrix) imagecols_aligned = imagecols_src.apply_similarity_transform(transform) # delete tmp folder diff --git a/limap/base/bindings.cc b/limap/base/bindings.cc index de914be8..4d40068d 100644 --- a/limap/base/bindings.cc +++ b/limap/base/bindings.cc @@ -9,7 +9,6 @@ namespace py = pybind11; #include "limap/util/kd_tree.h" #include "limap/util/types.h" -#include #include "limap/base/camera.h" #include "limap/base/camera_view.h" @@ -22,6 +21,11 @@ namespace py = pybind11; #include "limap/base/linetrack.h" #include "limap/base/pointtrack.h" +#include +#include +#include +#include + namespace limap { void bind_general_structures(py::module &m) { @@ -125,18 +129,6 @@ void bind_graph(py::module &m) { m.def("count_track_edges", &CountTrackEdges); } -void bind_transforms(py::module &m) { - py::class_(m, "SimilarityTransform3") - .def(py::init<>()) - .def(py::init(), py::arg("qvec"), py::arg("tvec"), - py::arg("scale") = 1.0) - .def(py::init(), py::arg("R"), py::arg("T"), - py::arg("scale") = 1.0) - .def("R", &SimilarityTransform3::R) - .def("T", &SimilarityTransform3::T) - .def("s", &SimilarityTransform3::s); -} - void bind_linebase(py::module &m) { py::class_(m, "Line2d", "A finite 2D line (segment).") .def(py::init<>(), R"( @@ -1292,7 +1284,7 @@ void bind_camera(py::module &m) { Apply similarity transform to all image poses. Args: - transform (:class:`limap.base.SimilarityTransform3`) + transform (:class:`limap.base.pycolmap.Sim3d`) )", py::arg("transform")) .def("get_first_image_id_by_camera_id", @@ -1322,8 +1314,6 @@ void bind_camera(py::module &m) { Returns: bool: True if all camera models are undistorted. )"); - - m.def("pose_similarity_transform", &pose_similarity_transform); } void bind_pointtrack(py::module &m) { @@ -1362,18 +1352,134 @@ void bind_pointtrack(py::module &m) { .def("count_images", &PointTrack::count_images); } +void bind_base_pycolmap(py::module &m_parent) { + using namespace colmap; + m_parent.def("get_effective_num_threads", &colmap::GetEffectiveNumThreads); + py::module_ m = m_parent.def_submodule("pycolmap"); + + // The following copied from pycolmap + py::class_ PyRotation3d(m, "Rotation3d", + py::module_local()); + PyRotation3d.def(py::init([]() { return Eigen::Quaterniond::Identity(); })) + .def(py::init(), "xyzw"_a, + "Quaternion in [x,y,z,w] format.") + .def(py::init(), "matrix"_a, + "3x3 rotation matrix.") + .def(py::init([](const Eigen::Vector3d &vec) { + return Eigen::Quaterniond( + Eigen::AngleAxis(vec.norm(), vec.normalized())); + }), + "axis_angle"_a, "Axis-angle 3D vector.") + .def_property( + "quat", py::overload_cast<>(&Eigen::Quaterniond::coeffs), + [](Eigen::Quaterniond &self, const Eigen::Vector4d &quat) { + self.coeffs() = quat; + }, + "Quaternion in [x,y,z,w] format.") + .def(py::self * Eigen::Quaterniond()) + .def(py::self * Eigen::Vector3d()) + .def("__mul__", + [](const Eigen::Quaterniond &self, + const py::EigenDRef &points) + -> Eigen::MatrixX3d { + return points * self.toRotationMatrix().transpose(); + }) + .def("normalize", &Eigen::Quaterniond::normalize) + .def("matrix", &Eigen::Quaterniond::toRotationMatrix) + .def("norm", &Eigen::Quaterniond::norm) + .def("angle", + [](const Eigen::Quaterniond &self) { + return Eigen::AngleAxis(self).angle(); + }) + .def( + "angle_to", + [](const Eigen::Quaterniond &self, const Eigen::Quaterniond &other) { + return self.angularDistance(other); + }, + "other"_a) + .def("inverse", &Eigen::Quaterniond::inverse) + .def("__repr__", [](const Eigen::Quaterniond &self) { + std::ostringstream ss; + ss << "Rotation3d(xyzw=[" << self.coeffs().format(vec_fmt) << "])"; + return ss.str(); + }); + py::implicitly_convertible(); + MakeDataclass(PyRotation3d); + + py::class_ PyRigid3d(m, "Rigid3d", py::module_local()); + PyRigid3d.def(py::init<>()) + .def(py::init(), + "rotation"_a, "translation"_a) + .def(py::init(&Rigid3d::FromMatrix), "matrix"_a, + "3x4 transformation matrix.") + .def_readwrite("rotation", &Rigid3d::rotation) + .def_readwrite("translation", &Rigid3d::translation) + .def("matrix", &Rigid3d::ToMatrix) + .def("adjoint", &Rigid3d::Adjoint) + .def(py::self * Rigid3d()) + .def(py::self * Eigen::Vector3d()) + .def("__mul__", + [](const Rigid3d &t, + const py::EigenDRef &points) + -> Eigen::MatrixX3d { + return (points * t.rotation.toRotationMatrix().transpose()) + .rowwise() + + t.translation.transpose(); + }) + .def("inverse", static_cast(&Inverse)) + .def("get_covariance_for_inverse", + static_cast( + &GetCovarianceForRigid3dInverse), + py::arg("covar")) + .def_static("interpolate", &InterpolateCameraPoses, "cam_from_world1"_a, + "cam_from_world2"_a, "t"_a); + py::implicitly_convertible(); + MakeDataclass(PyRigid3d); + + py::class_ PySim3d(m, "Sim3d", py::module_local()); + PySim3d.def(py::init<>()) + .def(py::init(), + "scale"_a, "rotation"_a, "translation"_a) + .def(py::init(&Sim3d::FromMatrix), "matrix"_a, + "3x4 transformation matrix.") + .def_property( + "scale", + [](Sim3d &self) { + return py::array({}, {}, &self.scale, py::cast(self)); + }, + [](Sim3d &self, double scale) { self.scale = scale; }) + .def_readwrite("rotation", &Sim3d::rotation) + .def_readwrite("translation", &Sim3d::translation) + .def("matrix", &Sim3d::ToMatrix) + .def(py::self * Sim3d()) + .def(py::self * Eigen::Vector3d()) + .def("__mul__", + [](const Sim3d &t, + const py::EigenDRef &points) + -> Eigen::MatrixX3d { + return (t.scale * + (points * t.rotation.toRotationMatrix().transpose())) + .rowwise() + + t.translation.transpose(); + }) + .def("transform_camera_world", &TransformCameraWorld, "cam_from_world"_a) + .def("inverse", static_cast(&Inverse)); + py::implicitly_convertible(); + MakeDataclass(PySim3d); +} + void bind_base(py::module &m) { bind_general_structures(m); bind_graph(m); - bind_transforms(m); bind_pointtrack(m); bind_camera(m); bind_linebase(m); bind_linetrack(m); bind_line_dists(m); bind_line_linker(m); - - m.def("get_effective_num_threads", &colmap::GetEffectiveNumThreads); + bind_base_pycolmap(m); } } // namespace limap diff --git a/limap/base/image_collection.cc b/limap/base/image_collection.cc index ec325f08..b224904f 100644 --- a/limap/base/image_collection.cc +++ b/limap/base/image_collection.cc @@ -461,10 +461,15 @@ double *ImageCollection::tvec_data(const int img_id) { } ImageCollection ImageCollection::apply_similarity_transform( - const SimilarityTransform3 &transform) const { + const colmap::Sim3d &transform) const { ImageCollection imagecols = ImageCollection(cameras, images); for (auto it = imagecols.images.begin(); it != imagecols.images.end(); ++it) { - it->second.pose = pose_similarity_transform(it->second.pose, transform); + // TODO: use colmap::TransformCameraWorld after we switched pose to + // colmap::Rigid3d + CameraPose &pose = it->second.pose; + M3D new_R = pose.R() * transform.rotation.toRotationMatrix().transpose(); + V3D new_T = transform.scale * pose.T() - new_R * transform.translation; + it->second.pose = CameraPose(new_R, new_T); } return imagecols; } diff --git a/limap/base/image_collection.h b/limap/base/image_collection.h index d3052ad8..51fd5530 100644 --- a/limap/base/image_collection.h +++ b/limap/base/image_collection.h @@ -11,12 +11,13 @@ namespace py = pybind11; +#include + #include "limap/_limap/helpers.h" #include "limap/util/types.h" #include "limap/base/camera.h" #include "limap/base/camera_view.h" -#include "limap/base/transforms.h" namespace limap { @@ -93,7 +94,7 @@ class ImageCollection { double *tvec_data(const int img_id); ImageCollection - apply_similarity_transform(const SimilarityTransform3 &transform) const; + apply_similarity_transform(const colmap::Sim3d &transform) const; // inverse indexing int get_first_image_id_by_camera_id(const int cam_id) const; diff --git a/limap/base/transforms.cc b/limap/base/transforms.cc deleted file mode 100644 index 40f30999..00000000 --- a/limap/base/transforms.cc +++ /dev/null @@ -1,12 +0,0 @@ -#include "limap/base/transforms.h" - -namespace limap { - -CameraPose pose_similarity_transform(const CameraPose &pose, - const SimilarityTransform3 &transform) { - M3D new_R = pose.R() * transform.R().transpose(); - V3D new_T = transform.s() * pose.T() - new_R * transform.T(); - return CameraPose(new_R, new_T); -} - -} // namespace limap diff --git a/limap/base/transforms.h b/limap/base/transforms.h deleted file mode 100644 index 5558f9df..00000000 --- a/limap/base/transforms.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include "limap/base/camera.h" -#include "limap/base/pose.h" - -namespace limap { - -class SimilarityTransform3 { - /* - * t_prime = R @ (s * t) + T - */ -public: - SimilarityTransform3() {} - SimilarityTransform3(V4D qqvec, V3D ttvec, double s = 1.0) - : qvec(qqvec), tvec(ttvec), scale(s) {} - SimilarityTransform3(M3D R, V3D T, double s = 1.0) : tvec(T), scale(s) { - qvec = RotationMatrixToQuaternion(R); - } - V4D qvec = V4D(1., 0., 0., 0.); - V3D tvec = V3D::Zero(); - double scale = 1.0; - - M3D R() const { return QuaternionToRotationMatrix(qvec); } - V3D T() const { return tvec; } - double s() const { return scale; } -}; - -CameraPose pose_similarity_transform(const CameraPose &pose, - const SimilarityTransform3 &transform); - -} // namespace limap diff --git a/third-party/pycolmap/helpers.h b/third-party/pycolmap/helpers.h new file mode 100644 index 00000000..986bf23e --- /dev/null +++ b/third-party/pycolmap/helpers.h @@ -0,0 +1,441 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace colmap; +using namespace pybind11::literals; +namespace py = pybind11; + +const Eigen::IOFormat vec_fmt(Eigen::StreamPrecision, + Eigen::DontAlignCols, + ", ", + ", "); + +template +T PyStringToEnum(const py::enum_& enm, const std::string& value) { + const auto values = enm.attr("__members__").template cast(); + const auto str_val = py::str(value); + if (!values.contains(str_val)) { + LOG(FATAL_THROW) << "Invalid string value " << value << " for enum " + << enm.attr("__name__").template cast(); + } + return T(values[str_val].template cast()); +} + +template +void AddStringToEnumConstructor(py::enum_& enm) { + enm.def(py::init([enm](const std::string& value) { + return PyStringToEnum(enm, py::str(value)); // str constructor + }), + "name"_a); + enm.attr("__repr__") = enm.attr("__str__"); + py::implicitly_convertible(); +} + +inline void UpdateFromDict(py::object& self, const py::dict& dict) { + for (const auto& it : dict) { + if (!py::isinstance(it.first)) { + LOG(FATAL_THROW) << "Dictionary key is not a string: " + << py::str(it.first); + } + const py::str name = py::reinterpret_borrow(it.first); + const py::handle& value = it.second; + const auto attr = self.attr(name); + try { + if (py::hasattr(attr, "mergedict") && py::isinstance(value)) { + attr.attr("mergedict").attr("__call__")(value); + } else { + self.attr(name) = value; + } + } catch (const py::error_already_set& ex) { + if (ex.matches(PyExc_TypeError)) { + // If fail we try bases of the class + const py::list bases = + attr.attr("__class__").attr("__bases__").cast(); + bool success_on_base = false; + for (auto& base : bases) { + try { + self.attr(name) = base(value); + success_on_base = true; + break; + } catch (const py::error_already_set&) { + continue; // We anyway throw afterwards + } + } + if (success_on_base) { + continue; + } + std::ostringstream ss; + ss << self.attr("__class__") + .attr("__name__") + .template cast() + << "." << name.template cast() << ": Could not convert " + << py::type::of(value.cast()) + .attr("__name__") + .template cast() + << ": " << py::str(value).template cast() << " to '" + << py::type::of(attr).attr("__name__").template cast() + << "'."; + // We write the err message to give info even if exceptions + // is catched outside, e.g. in function overload resolve + LOG(ERROR) << "Internal TypeError: " << ss.str(); + throw(py::type_error(std::string("Failed to merge dict into class: ") + + "Could not assign " + + name.template cast())); + } else if (ex.matches(PyExc_AttributeError) && + py::str(ex.value()).cast() == + std::string("can't set attribute")) { + std::ostringstream ss; + ss << self.attr("__class__") + .attr("__name__") + .template cast() + << "." << name.template cast() << " defined readonly."; + throw py::attribute_error(ss.str()); + } else if (ex.matches(PyExc_AttributeError)) { + LOG(ERROR) << "Internal AttributeError: " + << py::str(ex.value()).cast(); + throw; + } else { + LOG(ERROR) << "Internal Error: " + << py::str(ex.value()).cast(); + throw; + } + } + } +} + +inline bool AttributeIsFunction(const std::string& name, + const py::object& value) { + return (name.find("__") == 0 || name.rfind("__") != std::string::npos || + py::hasattr(value, "__func__") || py::hasattr(value, "__call__")); +} + +inline std::vector ListObjectAttributes(const py::object& pyself) { + std::vector attributes; + for (const auto& handle : pyself.attr("__dir__")()) { + const py::str attribute = py::reinterpret_borrow(handle); + const auto value = pyself.attr(attribute); + if (AttributeIsFunction(attribute, value)) { + continue; + } + attributes.push_back(attribute); + } + return attributes; +} + +template +py::dict ConvertToDict(const T& self, + std::vector attributes, + const bool recursive) { + const py::object pyself = py::cast(self); + if (attributes.empty()) { + attributes = ListObjectAttributes(pyself); + } + py::dict dict; + for (const auto& attr : attributes) { + const auto value = pyself.attr(attr.c_str()); + if (recursive && py::hasattr(value, "todict")) { + dict[attr.c_str()] = + value.attr("todict").attr("__call__")().template cast(); + } else { + dict[attr.c_str()] = value; + } + } + return dict; +} + +template +std::string CreateSummary(const T& self, bool write_type) { + std::ostringstream ss; + auto pyself = py::cast(self); + const std::string prefix = " "; + bool after_subsummary = false; + ss << pyself.attr("__class__").attr("__name__").template cast() + << ":"; + for (auto& handle : pyself.attr("__dir__")()) { + const py::str name = py::reinterpret_borrow(handle); + py::object attribute; + try { + attribute = pyself.attr(name); + } catch (const std::exception&) { + // Some properties are not valid for some uninitialized objects. + continue; + } + if (AttributeIsFunction(name, attribute)) { + continue; + } + ss << "\n"; + if (!after_subsummary) { + ss << prefix; + } + ss << name.template cast(); + if (py::hasattr(attribute, "summary")) { + std::string summ = attribute.attr("summary") + .attr("__call__")(write_type) + .template cast(); + static const std::regex newline_regex("\n"); + // NOLINTNEXTLINE(performance-inefficient-string-concatenation) + summ = std::regex_replace(summ, newline_regex, "\n" + prefix); + ss << ": " << summ; + } else { + if (write_type) { + const std::string type_str = + py::str(py::type::of(attribute).attr("__name__")); + ss << ": " << type_str; + after_subsummary = true; + } + std::string value = py::str(attribute); + if (value.length() > 80 && py::hasattr(attribute, "__len__")) { + const int length = attribute.attr("__len__")().template cast(); + value = StringPrintf( + "%c ... %d elements ... %c", value.front(), length, value.back()); + } + ss << " = " << value; + after_subsummary = false; + } + } + return ss.str(); +} + +template +std::string CreateRepresentationFromAttributes(const T& self) { + std::ostringstream ss; + auto pyself = py::cast(self); + ss << pyself.attr("__class__").attr("__name__").template cast() + << "("; + bool is_first = true; + for (auto& handle : pyself.attr("__dir__")()) { + const py::str name = py::reinterpret_borrow(handle); + py::object attribute; + try { + attribute = pyself.attr(name); + } catch (const std::exception&) { + // Some properties are not valid for some uninitialized objects. + continue; + } + if (AttributeIsFunction(name, attribute)) { + continue; + } + if (!is_first) { + ss << ", "; + } + is_first = false; + ss << name.template cast() << "="; + if (py::isinstance(attribute)) { + ss << "'" << py::str(attribute) << "'"; + } else { + ss << py::str(attribute); + } + } + ss << ")"; + return ss.str(); +} + +template +struct IsOstreamable : std::false_type {}; + +template +struct IsOstreamable< + T, + std::void_t() << std::declval())>> + : std::true_type {}; + +template +std::string CreateRepresentation(const T& self) { + if constexpr (IsOstreamable::value) { + std::ostringstream ss; + ss << self; + return ss.str(); + } else { + return CreateRepresentationFromAttributes(self); + } +} + +template +void AddDefaultsToDocstrings(py::class_ cls) { + auto obj = cls(); + for (auto& handle : obj.attr("__dir__")()) { + const std::string attribute = py::str(handle); + py::object member; + try { + member = obj.attr(attribute.c_str()); + } catch (const std::exception&) { + // 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)", + py::str(prop.doc()).cast().c_str(), + type_name.template cast().c_str(), + py::str(member).cast().c_str()); + prop.doc() = py::str(doc); + } +} + +template +void MakeDataclass(py::class_ cls, + const std::vector& attributes = {}) { + AddDefaultsToDocstrings(cls); + if (!py::hasattr(cls, "summary")) { + cls.def("summary", &CreateSummary, "write_type"_a = false); + } + if (!cls.attr("__dict__").contains("__repr__")) { + cls.def("__repr__", &CreateRepresentation); + } + cls.def("mergedict", &UpdateFromDict, "kwargs"_a); + cls.def( + "todict", + [attributes](const T& self, const bool recursive) { + return ConvertToDict(self, attributes, recursive); + }, + "recursive"_a = true); + + cls.def(py::init([cls](const py::dict& dict) { + py::object self = cls(); + self.attr("mergedict").attr("__call__")(dict); + return self.cast(); + }), + "kwargs"_a); + cls.def(py::init([cls](const py::kwargs& kwargs) { + py::dict dict = kwargs.cast(); + return cls(dict).template cast(); + })); + py::implicitly_convertible(); + py::implicitly_convertible(); + + cls.def("__copy__", [](const T& self) { return T(self); }); + cls.def("__deepcopy__", + [](const T& self, const py::dict&) { return T(self); }); + + cls.def(py::pickle( + [attributes](const T& self) { + return ConvertToDict(self, attributes, /*recursive=*/false); + }, + [cls](const py::dict& dict) { + py::object self = cls(); + self.attr("mergedict").attr("__call__")(dict); + return self.cast(); + })); +} + +// Catch python keyboard interrupts + +/* +// single +if (PyInterrupt().Raised()) { + // stop the execution and raise an exception + throw py::error_already_set(); +} + +// loop +PyInterrupt py_interrupt = PyInterrupt(2.0) +for (...) { + if (py_interrupt.Raised()) { + // stop the execution and raise an exception + throw py::error_already_set(); + } + // Do your workload here +} + + +*/ +struct PyInterrupt { + using clock = std::chrono::steady_clock; + using sec = std::chrono::duration; + explicit PyInterrupt(double gap = -1.0) : gap_(gap), start(clock::now()) {} + + inline bool Raised(); + + private: + std::mutex mutex_; + bool found = false; + Timer timer_; + clock::time_point start; + double gap_; +}; + +inline bool PyInterrupt::Raised() { + const sec duration = clock::now() - start; + if (!found && duration.count() > gap_) { + std::lock_guard lock(mutex_); + py::gil_scoped_acquire acq; + found = (PyErr_CheckSignals() != 0); + start = clock::now(); + } + return found; +} + +// Instead of thread.Wait() call this to allow interrupts through python +inline void PyWait(Thread* thread, double gap = 2.0) { + PyInterrupt py_interrupt(gap); + while (thread->IsRunning()) { + if (py_interrupt.Raised()) { + LOG(ERROR) << "Stopping thread..."; + thread->Stop(); + thread->Wait(); + throw py::error_already_set(); + } + } + // after finishing join the thread to avoid abort + thread->Wait(); +} + +// Test if pyceres is available +inline bool IsPyceresAvailable() { + try { + py::module::import("pyceres"); + } catch (const py::error_already_set&) { + return false; + } + return true; +} + +template +inline void DefDeprecation( + Parent& parent, + std::string old_name, + std::string new_name, + std::optional custom_warning = std::nullopt) { + const std::string doc = + StringPrintf("Deprecated, use ``%s`` instead.", new_name.c_str()); + parent.def( + old_name.c_str(), + [parent, + old_name, + new_name = std::move(new_name), + custom_warning = std::move(custom_warning)](const py::args& args, + const py::kwargs& kwargs) { + if (custom_warning) { + PyErr_WarnEx(PyExc_DeprecationWarning, custom_warning->c_str(), 1); + } else { + std::ostringstream warning; + warning << old_name << "() is deprecated, use " << new_name + << "() instead."; + PyErr_WarnEx(PyExc_DeprecationWarning, warning.str().c_str(), 1); + } + return parent.attr(new_name.c_str())(*args, **kwargs); + }, + doc.c_str()); +}