diff --git a/core/include/vc/core/types/Transforms.hpp b/core/include/vc/core/types/Transforms.hpp index 24164bbc1..0dc3e2aba 100644 --- a/core/include/vc/core/types/Transforms.hpp +++ b/core/include/vc/core/types/Transforms.hpp @@ -46,6 +46,9 @@ namespace volcart class Transform3D { public: + /** @brief Transform type string constant */ + static constexpr std::string_view TYPE{"Transform3D"}; + /** Pointer type */ using Pointer = std::shared_ptr; @@ -60,7 +63,7 @@ class Transform3D auto operator=(Transform3D&& other) -> Transform3D& = delete; /** @brief Return a string representation of the transform type */ - [[nodiscard]] virtual auto type() const -> std::string = 0; + [[nodiscard]] virtual auto type() const -> std::string_view = 0; /** @brief Clone the transform */ [[nodiscard]] virtual auto clone() const -> Pointer = 0; @@ -68,6 +71,8 @@ class Transform3D [[nodiscard]] virtual auto invertible() const -> bool; /** @brief Return the inverted transform */ [[nodiscard]] virtual auto invert() const -> Pointer; + /** @brief Return whether the underlying transform is composable */ + [[nodiscard]] virtual auto composable() const -> bool; /** * @brief Reset the transform parameters @@ -79,7 +84,7 @@ class Transform3D */ virtual void reset() = 0; /** @brief Clears all parameters and properties of the transform */ - virtual void clear(); + void clear(); /** * @brief Set the identifier for the source space @@ -134,6 +139,33 @@ class Transform3D [[nodiscard]] auto applyPointAndNormal( const cv::Vec6d& ptN, bool normalize = true) const -> cv::Vec6d; + /** + * @brief Compose two transforms into a single new transform + * + * Returns a pair of transform pointers. If the composition fails, the pair + * will contain pointers to both of the original inputs. If the second + * value in the pair is nullptr, then the composition was successful, and + * the new transform is available from the first pointer. + * + * @code{.cpp} + * // Two transforms + * Transform3D::Pointer lhs = AffineTransform::New(); + * Transform3D::Pointer rhs = AffineTransform::New(); + * + * // Compose and assign the results to existing variables + * std::tie(lhs, rhs) = Transform3D::Compose(lhs, rhs); + * + * // Check the result + * if(rhs) { + * std::cout << "Failed to compose transforms!\n"; + * } else { + * std::cout << "Composition successful!\n"; + * } + * @endcode + */ + static auto Compose(const Pointer& lhs, const Pointer& rhs) + -> std::pair; + /** @brief Save a transform to a JSON file */ static void Save(const filesystem::path& path, const Pointer& transform); /** @brief Load a transform from a JSON file */ @@ -147,8 +179,20 @@ class Transform3D /** Only derived classes can copy */ auto operator=(const Transform3D& other) -> Transform3D& = default; + /** + * Helper compose function. The base implementation returns a nullptr. + * Implementing derived classes should set the returned transform's source + * to this->source() and the target to rhs->target(). + */ + [[nodiscard]] virtual auto compose_(const Transform3D::Pointer& rhs) const + -> Transform3D::Pointer; + /** On-disk metadata type */ using Metadata = nlohmann::ordered_json; + /** Serialize the transform to metadata */ + static auto Serialize(const Pointer& transform) -> Metadata; + /** Deserialize the transform from metadata */ + static auto Deserialize(const Metadata& meta) -> Pointer; /** Serialize the derived class parameters */ virtual void to_meta_(Metadata& meta) = 0; /** Deserialize the derived class parameters */ @@ -161,6 +205,18 @@ class Transform3D std::string tgt_; }; +/** + * @brief Compose transform convenience operator + * + * Same as Transform3D::Compose but only returns the composed transform. If + * composition fails for any reason, will throw an exception. + * + * @throws std::invalid_argument if lhs or rhs are not composable + * @throws std::runtime_error if transform composition failed + */ +auto operator*(const Transform3D::Pointer& lhs, const Transform3D::Pointer& rhs) + -> Transform3D::Pointer; + /** * @brief 3D affine transform * @@ -169,7 +225,7 @@ class Transform3D * transform with the stored affine transform. For example, the following * transform will scale, rotate, and translate the 3D point, in that order: * - * @code + * @code{.cpp} * auto tfm = AffineTransform::New(); * // scale by 5 * tfm->scale(5); @@ -184,6 +240,9 @@ class Transform3D class AffineTransform : public Transform3D { public: + /** @copydoc Transform3D::TYPE */ + static constexpr std::string_view TYPE{"AffineTransform"}; + /** Parameters type: 4x4 matrix */ using Parameters = cv::Matx; @@ -194,17 +253,17 @@ class AffineTransform : public Transform3D static auto New() -> Pointer; /** @copydoc Transform3D::type() */ - [[nodiscard]] auto type() const -> std::string final; + [[nodiscard]] auto type() const -> std::string_view final; /** @copydoc Transform3D::clone() */ [[nodiscard]] auto clone() const -> Transform3D::Pointer final; /** @copydoc Transform3D::invertible() */ [[nodiscard]] auto invertible() const -> bool final; /** @copydoc Transform3D::invert() */ [[nodiscard]] auto invert() const -> Transform3D::Pointer final; + /** @copydoc Transform3D::composable() */ + [[nodiscard]] auto composable() const -> bool final; /** @copydoc Transform3D::reset() */ void reset() final; - /** @copydoc Transform3D::clear() */ - void clear() final; /** @copydoc Transform3D::applyPoint() */ [[nodiscard]] auto applyPoint(const cv::Vec3d& point) const @@ -269,6 +328,161 @@ class AffineTransform : public Transform3D AffineTransform() = default; /** Current parameters */ Parameters params_{cv::Matx::eye()}; + /** @copydoc Transform3D::compose_() */ + [[nodiscard]] auto compose_(const Transform3D::Pointer& rhs) const + -> Transform3D::Pointer final; + /** @copydoc Transform3D::to_meta_() */ + void to_meta_(Metadata& meta) final; + /** @copydoc Transform3D::from_meta_() */ + void from_meta_(const Metadata& meta) final; +}; + +/** + * @brief Identity transform + * + * Identity transform that simply returns input parameters. Useful for + * creating explicit mappings between a source and a target which share the + * same coordinate space. + * + * @code{.cpp} + * auto tfm = IdentityTransform::New(); + * auto pt = tfm->applyPoint({0, 1, 0}); // {0, 1, 0} + * @endcode + */ +class IdentityTransform : public Transform3D +{ +public: + /** @copydoc Transform3D::TYPE */ + static constexpr std::string_view TYPE{"IdentityTransform"}; + + /** Pointer type */ + using Pointer = std::shared_ptr; + + /** @brief Create a new IdentityTransform */ + static auto New() -> Pointer; + + /** @copydoc Transform3D::type() */ + [[nodiscard]] auto type() const -> std::string_view final; + /** @copydoc Transform3D::clone() */ + [[nodiscard]] auto clone() const -> Transform3D::Pointer final; + /** @copydoc Transform3D::invertible() */ + [[nodiscard]] auto invertible() const -> bool final; + /** @copydoc Transform3D::invert() */ + [[nodiscard]] auto invert() const -> Transform3D::Pointer final; + /** @copydoc Transform3D::composable() */ + [[nodiscard]] auto composable() const -> bool final; + /** @copydoc Transform3D::reset() */ + void reset() final; + + /** @copydoc Transform3D::applyPoint() */ + [[nodiscard]] auto applyPoint(const cv::Vec3d& point) const + -> cv::Vec3d final; + /** @copydoc Transform3D::applyVector() */ + [[nodiscard]] auto applyVector(const cv::Vec3d& vector) const + -> cv::Vec3d final; + +private: + /** Don't allow construction on the stack */ + IdentityTransform() = default; + /** @copydoc Transform3D::compose_() */ + [[nodiscard]] auto compose_(const Transform3D::Pointer& rhs) const + -> Transform3D::Pointer final; + /** @copydoc Transform3D::to_meta_() */ + void to_meta_(Metadata& meta) final; + /** @copydoc Transform3D::from_meta_() */ + void from_meta_(const Metadata& meta) final; +}; + +/** + * @brief Collection of transforms + * + * A convenience class which holds a list of transforms. When transforming + * points and vectors, each transform is applied sequentially to the input. + * + * @code{.cpp} + * // New transform + * auto tfm = CompositeTransform::New(); + * + * // Add some transforms + * auto t = AffineTransform::New(); + * t->translate(1, 2, 3); + * tfm->push_back(t); + * t->reset(); + * t->scale(4); + * tfm->push_back(t); + * + * // Apply all transforms to an input + * auto pt = tfm->applyPoint({0, 1, 0}); // {4, 12, 12} + * @endcode + * + * It can often be preferable, for both performance and numerical stability, to + * simplify all adjacent, composable transforms (e.g. AffineTransform, + * IdentityTransform) into a single transform. + * + * @code{.cpp} + * // Add some composable transforms + * tfm->push_back(AffineTransform::New()); + * tfm->push_back(IdentityTransform::New()); + * tfm->push_back(AffineTransform::New()); + * tfm->size(); // 3 + * + * // Simplify the transform + * tfm->simplify(); + * tfm->size(); // 1 + * @endcode + */ +class CompositeTransform : public Transform3D +{ +public: + /** @copydoc Transform3D::TYPE */ + static constexpr std::string_view TYPE{"CompositeTransform"}; + + /** Pointer type */ + using Pointer = std::shared_ptr; + + /** @brief Create a new CompositeTransform */ + static auto New() -> Pointer; + + /** @copydoc Transform3D::type() */ + [[nodiscard]] auto type() const -> std::string_view final; + /** @copydoc Transform3D::clone() */ + [[nodiscard]] auto clone() const -> Transform3D::Pointer final; + /** @copydoc Transform3D::reset() */ + void reset() final; + + /** @copydoc Transform3D::applyPoint() */ + [[nodiscard]] auto applyPoint(const cv::Vec3d& point) const + -> cv::Vec3d final; + /** @copydoc Transform3D::applyVector() */ + [[nodiscard]] auto applyVector(const cv::Vec3d& vector) const + -> cv::Vec3d final; + + /** + * @brief Add a transform to the end of the composite transform stack + * + * The transform is cloned before being added to the transform stack. If + * the transform is also a CompositeTransform, its transform stack is + * expanded and copied to the end of this transform's stack. + */ + void push_back(const Transform3D::Pointer& t); + + /** @brief Get the number of transforms in the composite transform */ + [[nodiscard]] auto size() const noexcept -> std::size_t; + + /** + * @brief Compose all composable transforms + * + * Simplifies the transform by composing all adjacent, composable + * transforms in the composite transform list. This can lead to better + * runtime performance and numerical stability for the apply functions. + */ + void simplify(); + +private: + /** Don't allow construction on the stack */ + CompositeTransform() = default; + /** Transform list */ + std::vector tfms_; /** @copydoc Transform3D::to_meta_() */ void to_meta_(Metadata& meta) final; /** @copydoc Transform3D::from_meta_() */ diff --git a/core/src/Transforms.cpp b/core/src/Transforms.cpp index ae8a744dd..fb77a4e40 100644 --- a/core/src/Transforms.cpp +++ b/core/src/Transforms.cpp @@ -16,7 +16,7 @@ namespace void WriteMetadata(const fs::path& path, const nlohmann::ordered_json& m) { std::ofstream o{path}; - o << m << std::endl; + o << m << "\n"; } auto LoadMetadata(const fs::path& path) -> nlohmann::ordered_json @@ -58,22 +58,39 @@ void Transform3D::clear() { src_.clear(); tgt_.clear(); + reset(); +} + +auto Transform3D::Compose(const Pointer& lhs, const Pointer& rhs) + -> std::pair +{ + // Return the inputs if either are not composable + if (not lhs->composable() or not rhs->composable()) { + return {lhs, rhs}; + } + + // Defer to the transform for composition + return {lhs->compose_(rhs), nullptr}; } -void Transform3D::Save( - const filesystem::path& path, const Transform3D::Pointer& transform) +auto Transform3D::compose_(const Pointer& rhs) const -> Transform3D::Pointer +{ + return nullptr; +} + +auto Transform3D::Serialize(const Pointer& transform) -> Metadata { Metadata meta{ {"type", "Transform3D"}, {"source", transform->src_}, - {"target", transform->tgt_}}; + {"target", transform->tgt_}, + {"transform-type", transform->type()}}; transform->to_meta_(meta); - ::WriteMetadata(path, meta); + return meta; } -auto Transform3D::Load(const filesystem::path& path) -> Transform3D::Pointer +auto Transform3D::Deserialize(const Metadata& meta) -> Pointer { - auto meta = ::LoadMetadata(path); if (meta["type"].get() != "Transform3D") { throw std::runtime_error("File not of type: Transform3D"); } @@ -83,9 +100,13 @@ auto Transform3D::Load(const filesystem::path& path) -> Transform3D::Pointer } auto tfmType = meta["transform-type"].get(); - Transform3D::Pointer result; - if (tfmType == "AffineTransform") { + Pointer result; + if (tfmType == AffineTransform::TYPE) { result = AffineTransform::New(); + } else if (tfmType == IdentityTransform::TYPE) { + result = IdentityTransform::New(); + } else if (tfmType == CompositeTransform::TYPE) { + result = CompositeTransform::New(); } else { throw std::runtime_error("Unknown transform type: " + tfmType); } @@ -96,18 +117,54 @@ auto Transform3D::Load(const filesystem::path& path) -> Transform3D::Pointer return result; } +void Transform3D::Save(const filesystem::path& path, const Pointer& transform) +{ + ::WriteMetadata(path, Serialize(transform)); +} + +auto Transform3D::Load(const filesystem::path& path) -> Pointer +{ + auto meta = ::LoadMetadata(path); + return Deserialize(meta); +} + auto Transform3D::invertible() const -> bool { return false; } -auto Transform3D::invert() const -> Transform3D::Pointer +auto Transform3D::invert() const -> Pointer { - throw std::runtime_error(this->type() + " is not invertible"); + throw std::runtime_error(std::string(this->type()) + " is not invertible"); +} + +auto Transform3D::composable() const -> bool { return false; } + +auto vc::operator*( + const Transform3D::Pointer& lhs, const Transform3D::Pointer& rhs) + -> Transform3D::Pointer +{ + // catch early + if (not lhs->composable()) { + throw std::invalid_argument( + "lhs transform is not composable: " + std::string(lhs->type())); + } + if (not rhs->composable()) { + throw std::invalid_argument( + "rhs transform is not composable: " + std::string(rhs->type())); + } + // try compose + auto [ret, should_be_null] = Transform3D::Compose(lhs, rhs); + // failed compose + if (should_be_null) { + throw std::runtime_error("Inputs could not be composed"); + } + // return lhs + return ret; } /////////////////////////////////////////// ///////////// AffineTransform ///////////// /////////////////////////////////////////// -auto AffineTransform::New() -> AffineTransform::Pointer +auto AffineTransform::New() -> Pointer { // Trick to allow classes with protected/private constructors to be // constructed with std::make_shared: https://stackoverflow.com/a/25069711 @@ -128,21 +185,14 @@ auto AffineTransform::applyVector(const cv::Vec3d& vector) const -> cv::Vec3d return {p[0], p[1], p[2]}; } -void AffineTransform::to_meta_(Metadata& meta) -{ - meta["transform-type"] = "AffineTransform"; - meta["params"] = params_; -} +void AffineTransform::to_meta_(Metadata& meta) { meta["params"] = params_; } void AffineTransform::from_meta_(const Metadata& meta) { params_ = meta["params"].get(); } -auto AffineTransform::params() const -> AffineTransform::Parameters -{ - return params_; -} +auto AffineTransform::params() const -> Parameters { return params_; } void AffineTransform::params(const Parameters& params) { params_ = params; } @@ -160,16 +210,40 @@ auto AffineTransform::invert() const -> Transform3D::Pointer return inverted; } -auto AffineTransform::type() const -> std::string { return "AffineTransform"; } +auto AffineTransform::composable() const -> bool { return true; } -void AffineTransform::reset() { params_ = Parameters::eye(); } - -void AffineTransform::clear() +auto AffineTransform::compose_(const Transform3D::Pointer& rhs) const + -> Transform3D::Pointer { - Transform3D::clear(); - reset(); + // Get a copy of self + auto res = AffineTransform::New(); + + // Copy Transform3D parameters + res->source(source()); + res->target(rhs->target()); + + // If IdentityTransform + if (rhs->type() == IdentityTransform::TYPE) { + res->params_ = params_; + } else if (rhs->type() == AffineTransform::TYPE) { + // Compose the parameters + auto affRhs = std::dynamic_pointer_cast(rhs); + if (not affRhs) { + throw std::invalid_argument("rhs argument is not AffineTransform"); + } + res->params_ = affRhs->params_ * params_; + } else { + throw std::invalid_argument( + "Unsupported transform type: " + std::string(rhs->type())); + } + + return res; } +auto AffineTransform::type() const -> std::string_view { return TYPE; } + +void AffineTransform::reset() { params_ = Parameters::eye(); } + auto AffineTransform::clone() const -> Transform3D::Pointer { return std::make_shared(*this); @@ -237,6 +311,177 @@ auto operator<<(std::ostream& os, const AffineTransform& t) -> std::ostream& return os; } +///////////////////////////////////////////// +///////////// IdentityTransform ///////////// +///////////////////////////////////////////// + +auto IdentityTransform::New() -> Pointer +{ + struct EnableSharedHelper : public IdentityTransform { + }; + return std::make_shared(); +} + +auto IdentityTransform::type() const -> std::string_view { return TYPE; } + +auto IdentityTransform::clone() const -> Transform3D::Pointer +{ + return std::make_shared(*this); +} + +auto IdentityTransform::invertible() const -> bool { return true; } + +auto IdentityTransform::invert() const -> Transform3D::Pointer +{ + auto ret = New(); + ret->source(target()); + ret->target(source()); + return ret; +} + +auto IdentityTransform::composable() const -> bool { return true; } + +auto IdentityTransform::compose_(const Transform3D::Pointer& rhs) const + -> Transform3D::Pointer +{ + auto res = rhs->clone(); + res->source(source()); + + return res; +} + +void IdentityTransform::reset() {} + +auto IdentityTransform::applyPoint(const cv::Vec3d& point) const -> cv::Vec3d +{ + return point; +} + +auto IdentityTransform::applyVector(const cv::Vec3d& vector) const -> cv::Vec3d +{ + return vector; +} + +void IdentityTransform::to_meta_(Metadata& meta) {} + +void IdentityTransform::from_meta_(const Metadata& meta) {} + +////////////////////////////////////////////// +///////////// CompositeTransform ///////////// +////////////////////////////////////////////// + +auto CompositeTransform::New() -> Pointer +{ + struct EnableSharedHelper : public CompositeTransform { + }; + return std::make_shared(); +} + +auto CompositeTransform::type() const -> std::string_view { return TYPE; } + +auto CompositeTransform::clone() const -> Transform3D::Pointer +{ + return std::make_shared(*this); +} + +void CompositeTransform::reset() { tfms_.clear(); } + +auto CompositeTransform::applyPoint(const cv::Vec3d& point) const -> cv::Vec3d +{ + auto pt = point; + for (const auto& t : tfms_) { + pt = t->applyPoint(pt); + } + return pt; +} + +auto CompositeTransform::applyVector(const cv::Vec3d& vector) const -> cv::Vec3d +{ + auto vec = vector; + for (const auto& t : tfms_) { + vec = t->applyVector(vec); + } + return vector; +} + +void CompositeTransform::push_back(const Transform3D::Pointer& t) +{ + // Easy case: Not a composite transform + if (t->type() != CompositeTransform::TYPE) { + tfms_.push_back(t->clone()); + return; + } + + // Hard case: Composite transforms should be expanded + std::list queue{t}; + while (not queue.empty()) { + // Get the next transform + auto tfm = queue.front(); + queue.pop_front(); + + // If a composite transform, push its stack to the front of the queue + if (tfm->type() == CompositeTransform::TYPE) { + auto cmp = std::dynamic_pointer_cast(t); + queue.insert(queue.begin(), cmp->tfms_.begin(), cmp->tfms_.end()); + } + // Otherwise, add this tfm to the queue + else { + tfms_.push_back(t->clone()); + } + } +} + +auto CompositeTransform::size() const noexcept -> std::size_t +{ + return tfms_.size(); +} + +void CompositeTransform::simplify() +{ + std::vector newTfms; + Transform3D::Pointer lhs; + Transform3D::Pointer rhs; + for (const auto& tfm : tfms_) { + // Set our first LHS + if (not lhs) { + lhs = tfm; + continue; + } + + // Try compose + std::tie(lhs, rhs) = Compose(lhs, tfm); + + // if rhs, then couldn't compose + // push lhs onto new list. rhs is new lhs. + if (rhs) { + newTfms.push_back(lhs); + lhs = rhs; + } + } + // push the final lhs + newTfms.push_back(lhs); + + // replace the current list + tfms_ = newTfms; +} + +void CompositeTransform::to_meta_(Metadata& meta) +{ + Metadata stack; + for (const auto& tfm : tfms_) { + stack.push_back(Serialize(tfm)); + } + meta["transform-stack"] = stack; +} + +void CompositeTransform::from_meta_(const Metadata& meta) +{ + tfms_.clear(); + for (const auto& m : meta["transform-stack"]) { + push_back(Deserialize(m)); + } +} + ////////////////////////////////////////// ///////////// ApplyTransform ///////////// ////////////////////////////////////////// @@ -310,4 +555,4 @@ auto vc::ApplyTransform( bool normalize) -> PerPixelMap::Pointer { return PerPixelMap::New(ApplyTransform(*ppm, transform, normalize)); -} +} \ No newline at end of file diff --git a/core/test/TransformsTest.cpp b/core/test/TransformsTest.cpp index 307336fe5..10842194e 100644 --- a/core/test/TransformsTest.cpp +++ b/core/test/TransformsTest.cpp @@ -8,6 +8,10 @@ using namespace volcart; using namespace volcart::testing; namespace fs = volcart::filesystem; +/////////////////////////////////////////// +///////////// AffineTransform ///////////// +/////////////////////////////////////////// + TEST(Transform, AffineClone) { // Create transform @@ -17,9 +21,9 @@ TEST(Transform, AffineClone) tfm->rotate(90, cv::Vec3d{0, 0, 1}); tfm->translate(1, 2, 3); - AffineTransform test = *tfm; + // Clone + auto result = std::dynamic_pointer_cast(tfm->clone()); - auto result = std::static_pointer_cast(tfm->clone()); // Compare equality EXPECT_EQ(result->type(), tfm->type()); EXPECT_EQ(result->source(), tfm->source()); @@ -38,11 +42,11 @@ TEST(Transforms, AffineSerialization) // Write to disk const fs::path path{"vc_core_Transforms_AffineTransform.json"}; - AffineTransform::Save(path, tfm); + Transform3D::Save(path, tfm); // Read from disk auto result = - std::static_pointer_cast(AffineTransform::Load(path)); + std::dynamic_pointer_cast(AffineTransform::Load(path)); // Compare equality EXPECT_EQ(result->type(), tfm->type()); @@ -238,4 +242,370 @@ TEST(Transforms, AffineInvert) inv = tfm->invert(); result = inv->applyPoint(result); SmallOrClose(result, orig); +} + +///////////////////////////////////////////// +///////////// IdentityTransform ///////////// +///////////////////////////////////////////// + +TEST(Transform, IdentityClone) +{ + // Create transform + auto tfm = IdentityTransform::New(); + tfm->source("abcdefgh"); + tfm->target("ijklmnop"); + + // Clone + auto result = std::dynamic_pointer_cast(tfm->clone()); + + // Compare equality + EXPECT_EQ(result->type(), tfm->type()); + EXPECT_EQ(result->source(), tfm->source()); + EXPECT_EQ(result->target(), tfm->target()); +} + +TEST(Transforms, IdentitySerialization) +{ + // Create transform + auto tfm = IdentityTransform::New(); + tfm->source("abcdefgh"); + tfm->target("ijklmnop"); + + // Write to disk + const fs::path path{"vc_core_Transforms_IdentityTransform.json"}; + Transform3D::Save(path, tfm); + + // Read from disk + auto result = + std::dynamic_pointer_cast(Transform3D::Load(path)); + + // Compare equality + EXPECT_EQ(result->type(), tfm->type()); + EXPECT_EQ(result->source(), tfm->source()); + EXPECT_EQ(result->target(), tfm->target()); +} + +TEST(Transforms, IdentityResetClear) +{ + // Build a transform + auto tfm = IdentityTransform::New(); + tfm->source("abc"); + tfm->target("def"); + + // Test that reset doesn't affect source/target + tfm->reset(); + EXPECT_EQ(tfm->source(), "abc"); + EXPECT_EQ(tfm->target(), "def"); + + // Test that clear resets everything + tfm->clear(); + EXPECT_EQ(tfm->source(), ""); + EXPECT_EQ(tfm->target(), ""); +} + +TEST(Transforms, IdentityApplyAndInvert) +{ + // Original point + const cv::Vec3d orig{0, 1, 1}; + + // Get forward transform + auto tfm = IdentityTransform::New(); + tfm->source("abc"); + tfm->target("def"); + + // Get inverse transform + EXPECT_TRUE(tfm->invertible()); + auto inv = tfm->invert(); + EXPECT_EQ(inv->source(), tfm->target()); + EXPECT_EQ(inv->target(), tfm->source()); + + // Test apply point + auto result = tfm->applyPoint(orig); + EXPECT_EQ(result, orig); + result = inv->applyPoint(result); + EXPECT_EQ(result, orig); + + // Test apply vector + result = tfm->applyVector(orig); + EXPECT_EQ(result, orig); + result = inv->applyVector(result); + EXPECT_EQ(result, orig); +} + +/////////////////////////////////// +///////////// Compose ///////////// +/////////////////////////////////// + +TEST(Transforms, ComposeFunction) +{ + /// Affine-to-Affine /// + // Expected transform + auto tfmA = AffineTransform::New(); + tfmA->source("a"); + tfmA->target("c"); + tfmA->scale(2, 3, 4); + tfmA->translate(1, 2, 3); + EXPECT_TRUE(tfmA->composable()); + + // Setup good transforms + auto lhsA = AffineTransform::New(); + lhsA->source("a"); + lhsA->target("b"); + lhsA->scale(2, 3, 4); + auto rhsA = AffineTransform::New(); + rhsA->source("b"); + rhsA->target("c"); + rhsA->translate(1, 2, 3); + + // Compose + auto [res, should_be_null] = Transform3D::Compose(lhsA, rhsA); + + // Verify composition worked correctly + EXPECT_FALSE(should_be_null); + + // Compare equality + auto resultA = std::dynamic_pointer_cast(res); + EXPECT_EQ(resultA->type(), tfmA->type()); + EXPECT_EQ(resultA->source(), tfmA->source()); + EXPECT_EQ(resultA->target(), tfmA->target()); + EXPECT_EQ(resultA->params(), tfmA->params()); + + /// Identity-to-Identity /// + auto tfmI = IdentityTransform::New(); + tfmI->source("a"); + tfmI->target("c"); + EXPECT_TRUE(tfmI->composable()); + + auto lhsI = IdentityTransform::New(); + lhsI->source("a"); + lhsI->target("b"); + auto rhsI = IdentityTransform::New(); + rhsI->source("b"); + rhsI->target("c"); + + // Compose + std::tie(res, should_be_null) = Transform3D::Compose(lhsI, rhsI); + + // Verify composition worked correctly + EXPECT_FALSE(should_be_null); + + // Compare equality + auto resultI = std::dynamic_pointer_cast(res); + EXPECT_EQ(resultI->type(), tfmI->type()); + EXPECT_EQ(resultI->source(), tfmI->source()); + EXPECT_EQ(resultI->target(), tfmI->target()); + + /// Affine-to-Identity /// + tfmA = std::dynamic_pointer_cast(lhsA->clone()); + tfmA->source("a"); + tfmA->target("c"); + + // Compose + std::tie(res, should_be_null) = Transform3D::Compose(lhsA, rhsI); + + // Verify composition worked correctly + EXPECT_FALSE(should_be_null); + + // Compare equality + resultA = std::dynamic_pointer_cast(res); + EXPECT_EQ(resultA->type(), tfmA->type()); + EXPECT_EQ(resultA->source(), tfmA->source()); + EXPECT_EQ(resultA->target(), tfmA->target()); + EXPECT_EQ(resultA->params(), tfmA->params()); + + /// Identity-to-Affine /// + tfmA = std::dynamic_pointer_cast(rhsA->clone()); + tfmA->source("a"); + tfmA->target("c"); + + // Compose + std::tie(res, should_be_null) = Transform3D::Compose(lhsI, rhsA); + + // Verify composition worked correctly + EXPECT_FALSE(should_be_null); + + // Compare equality + resultA = std::dynamic_pointer_cast(res); + EXPECT_EQ(resultA->type(), tfmA->type()); + EXPECT_EQ(resultA->source(), tfmA->source()); + EXPECT_EQ(resultA->target(), tfmA->target()); + EXPECT_EQ(resultA->params(), tfmA->params()); + + /// Composite-to-Anything /// + auto tfmC = CompositeTransform::New(); + EXPECT_FALSE(tfmC->composable()); + + Transform3D::Pointer should_not_be_null; + std::tie(res, should_not_be_null) = Transform3D::Compose(tfmC, rhsA); + EXPECT_TRUE(should_not_be_null); + std::tie(res, should_not_be_null) = Transform3D::Compose(lhsI, tfmC); + EXPECT_TRUE(should_not_be_null); +} + +TEST(Transforms, ComposeOperator) +{ + // Expected transform + auto tfm = AffineTransform::New(); + tfm->source("a"); + tfm->target("c"); + tfm->scale(2, 3, 4); + tfm->translate(1, 2, 3); + + // Set up good transforms + auto lhs = AffineTransform::New(); + lhs->source("a"); + lhs->target("b"); + lhs->scale(2, 3, 4); + auto rhs = AffineTransform::New(); + rhs->source("b"); + rhs->target("c"); + rhs->translate(1, 2, 3); + + // Compose + Transform3D::Pointer res; + EXPECT_NO_THROW(res = lhs * rhs); + + // Compare equality + auto result = std::dynamic_pointer_cast(res); + EXPECT_EQ(result->type(), tfm->type()); + EXPECT_EQ(result->source(), tfm->source()); + EXPECT_EQ(result->target(), tfm->target()); + EXPECT_EQ(result->params(), tfm->params()); + + auto lhsC = CompositeTransform::New(); + EXPECT_THROW(res = lhsC * rhs, std::invalid_argument); +} + +////////////////////////////////////////////// +///////////// CompositeTransform ///////////// +////////////////////////////////////////////// + +TEST(Transform, CompositeClone) +{ + // Create transform + auto tfm = CompositeTransform::New(); + tfm->source("abcdefgh"); + tfm->target("ijklmnop"); + tfm->push_back(AffineTransform::New()); + tfm->push_back(IdentityTransform::New()); + tfm->push_back(AffineTransform::New()); + + // Clone + auto result = std::dynamic_pointer_cast(tfm->clone()); + + // Compare equality + EXPECT_EQ(result->type(), tfm->type()); + EXPECT_EQ(result->source(), tfm->source()); + EXPECT_EQ(result->target(), tfm->target()); + EXPECT_EQ(result->size(), tfm->size()); +} + +TEST(Transforms, CompositeSerialization) +{ + // Create transform + auto tfm = CompositeTransform::New(); + tfm->source("abcdefgh"); + tfm->target("ijklmnop"); + + tfm->push_back(AffineTransform::New()); + tfm->push_back(IdentityTransform::New()); + + // Write to disk + const fs::path path{"vc_core_Transforms_CompositeTransform.json"}; + Transform3D::Save(path, tfm); + + // Read from disk + auto result = + std::dynamic_pointer_cast(Transform3D::Load(path)); + + // Compare equality + EXPECT_EQ(result->type(), tfm->type()); + EXPECT_EQ(result->source(), tfm->source()); + EXPECT_EQ(result->target(), tfm->target()); + EXPECT_EQ(result->size(), tfm->size()); +} + +TEST(Transforms, CompositeApply) +{ + // Set up composite transform + auto tfm = CompositeTransform::New(); + auto affine = AffineTransform::New(); + affine->scale(5); + tfm->push_back(affine); + tfm->push_back(IdentityTransform::New()); + affine->reset(); + affine->rotate(90, 0, 0, 1); + tfm->push_back(affine); + tfm->push_back(IdentityTransform::New()); + affine->reset(); + affine->translate(0, 10, 0); + tfm->push_back(affine); + tfm->push_back(IdentityTransform::New()); + + auto result = tfm->applyPoint({0, 1, 0}); + SmallOrClose(result, {-5., 10., 0.}); + + // Test simplify + auto origSize = tfm->size(); + tfm->simplify(); + EXPECT_NE(tfm->size(), origSize); + EXPECT_EQ(tfm->size(), 1); + + result = tfm->applyPoint({0, 1, 0}); + SmallOrClose(result, {-5., 10., 0.}); +} + +TEST(Transforms, CompositeExplicitInvert) +{ + auto tfm = CompositeTransform::New(); + auto a = AffineTransform::New(); + a->translate(1, 2, 3); + a->scale(5); + a->rotate(90, 0, 0, 1); + + tfm->push_back(a); + tfm->push_back(a->invert()); + + auto res = tfm->applyPoint({0, 0, 0}); + SmallOrClose(res, {0, 0, 0}); + + tfm->simplify(); + res = tfm->applyPoint({0, 0, 0}); + SmallOrClose(res, {0, 0, 0}); +} + +TEST(Transforms, CompositePushComposite) +{ + auto outer = CompositeTransform::New(); + + auto inner = CompositeTransform::New(); + inner->push_back(AffineTransform::New()); + inner->push_back(IdentityTransform::New()); + inner->push_back(AffineTransform::New()); + inner->push_back(IdentityTransform::New()); + inner->push_back(AffineTransform::New()); + inner->push_back(IdentityTransform::New()); + inner->push_back(AffineTransform::New()); + inner->push_back(IdentityTransform::New()); + + EXPECT_EQ(outer->size(), 0); + outer->push_back(inner); + EXPECT_EQ(outer->size(), inner->size()); +} + +TEST(Transforms, CompositeReset) +{ + auto tfm = CompositeTransform::New(); + tfm->push_back(AffineTransform::New()); + tfm->push_back(IdentityTransform::New()); + tfm->push_back(AffineTransform::New()); + tfm->push_back(IdentityTransform::New()); + tfm->push_back(AffineTransform::New()); + tfm->push_back(IdentityTransform::New()); + tfm->push_back(AffineTransform::New()); + tfm->push_back(IdentityTransform::New()); + EXPECT_EQ(tfm->size(), 8); + + tfm->reset(); + EXPECT_EQ(tfm->size(), 0); } \ No newline at end of file diff --git a/examples/files/affine-transform.json b/examples/files/affine-transform.json new file mode 100644 index 000000000..6105fa691 --- /dev/null +++ b/examples/files/affine-transform.json @@ -0,0 +1,10 @@ +{ + "type": "Transform3D", + "source": "20231128010200", + "target": "20231127120000", + "transform-type": "AffineTransform", + "params": [[1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0]] +} \ No newline at end of file diff --git a/examples/files/composite-transform.json b/examples/files/composite-transform.json new file mode 100644 index 000000000..c1883800c --- /dev/null +++ b/examples/files/composite-transform.json @@ -0,0 +1,18 @@ +{ + "type": "Transform3D", + "source": "20231128010200", + "target": "20231127120000", + "transform-type": "CompositeTransform", + "transform-stack": [ + { + "type": "Transform3D", + "source": "not-used", + "target": "not-used", + "transform-type": "AffineTransform", + "params": [[1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0]] + } + ] +} \ No newline at end of file diff --git a/examples/files/identity-transform.json b/examples/files/identity-transform.json new file mode 100644 index 000000000..6c57479fe --- /dev/null +++ b/examples/files/identity-transform.json @@ -0,0 +1,6 @@ +{ + "type": "Transform3D", + "source": "20231128010200", + "target": "20231127120000", + "transform-type": "IdentityTransform" +} \ No newline at end of file diff --git a/testing/test/res/Testing.volpkg/transforms/affine-transform.json b/testing/test/res/Testing.volpkg/transforms/affine-transform.json new file mode 100644 index 000000000..6105fa691 --- /dev/null +++ b/testing/test/res/Testing.volpkg/transforms/affine-transform.json @@ -0,0 +1,10 @@ +{ + "type": "Transform3D", + "source": "20231128010200", + "target": "20231127120000", + "transform-type": "AffineTransform", + "params": [[1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0]] +} \ No newline at end of file diff --git a/testing/test/res/Testing.volpkg/transforms/composite-transform.json b/testing/test/res/Testing.volpkg/transforms/composite-transform.json new file mode 100644 index 000000000..c1883800c --- /dev/null +++ b/testing/test/res/Testing.volpkg/transforms/composite-transform.json @@ -0,0 +1,18 @@ +{ + "type": "Transform3D", + "source": "20231128010200", + "target": "20231127120000", + "transform-type": "CompositeTransform", + "transform-stack": [ + { + "type": "Transform3D", + "source": "not-used", + "target": "not-used", + "transform-type": "AffineTransform", + "params": [[1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0]] + } + ] +} \ No newline at end of file diff --git a/testing/test/res/Testing.volpkg/transforms/identity-transform.json b/testing/test/res/Testing.volpkg/transforms/identity-transform.json new file mode 100644 index 000000000..6c57479fe --- /dev/null +++ b/testing/test/res/Testing.volpkg/transforms/identity-transform.json @@ -0,0 +1,6 @@ +{ + "type": "Transform3D", + "source": "20231128010200", + "target": "20231127120000", + "transform-type": "IdentityTransform" +} \ No newline at end of file