@@ -46,6 +46,9 @@ namespace volcart
4646class Transform3D
4747{
4848public:
49+ /* * @brief Transform type string constant */
50+ static constexpr std::string_view TYPE{" Transform3D" };
51+
4952 /* * Pointer type */
5053 using Pointer = std::shared_ptr<Transform3D>;
5154
@@ -60,14 +63,16 @@ class Transform3D
6063 auto operator =(Transform3D&& other) -> Transform3D& = delete ;
6164
6265 /* * @brief Return a string representation of the transform type */
63- [[nodiscard]] virtual auto type () const -> std::string = 0;
66+ [[nodiscard]] virtual auto type () const -> std::string_view = 0;
6467 /* * @brief Clone the transform */
6568 [[nodiscard]] virtual auto clone () const -> Pointer = 0;
6669
6770 /* * @brief Return whether the underlying transform is invertible */
6871 [[nodiscard]] virtual auto invertible () const -> bool;
6972 /* * @brief Return the inverted transform */
7073 [[nodiscard]] virtual auto invert () const -> Pointer;
74+ /* * @brief Return whether the underlying transform is composable */
75+ [[nodiscard]] virtual auto composable () const -> bool;
7176
7277 /* *
7378 * @brief Reset the transform parameters
@@ -79,7 +84,7 @@ class Transform3D
7984 */
8085 virtual void reset () = 0;
8186 /* * @brief Clears all parameters and properties of the transform */
82- virtual void clear ();
87+ void clear ();
8388
8489 /* *
8590 * @brief Set the identifier for the source space
@@ -134,6 +139,33 @@ class Transform3D
134139 [[nodiscard]] auto applyPointAndNormal (
135140 const cv::Vec6d& ptN, bool normalize = true ) const -> cv::Vec6d;
136141
142+ /* *
143+ * @brief Compose two transforms into a single new transform
144+ *
145+ * Returns a pair of transform pointers. If the composition fails, the pair
146+ * will contain pointers to both of the original inputs. If the second
147+ * value in the pair is nullptr, then the composition was successful, and
148+ * the new transform is available from the first pointer.
149+ *
150+ * @code{.cpp}
151+ * // Two transforms
152+ * Transform3D::Pointer lhs = AffineTransform::New();
153+ * Transform3D::Pointer rhs = AffineTransform::New();
154+ *
155+ * // Compose and assign the results to existing variables
156+ * std::tie(lhs, rhs) = Transform3D::Compose(lhs, rhs);
157+ *
158+ * // Check the result
159+ * if(rhs) {
160+ * std::cout << "Failed to compose transforms!\n";
161+ * } else {
162+ * std::cout << "Composition successful!\n";
163+ * }
164+ * @endcode
165+ */
166+ static auto Compose (const Pointer& lhs, const Pointer& rhs)
167+ -> std::pair<Pointer, Pointer>;
168+
137169 /* * @brief Save a transform to a JSON file */
138170 static void Save (const filesystem::path& path, const Pointer& transform);
139171 /* * @brief Load a transform from a JSON file */
@@ -147,8 +179,20 @@ class Transform3D
147179 /* * Only derived classes can copy */
148180 auto operator =(const Transform3D& other) -> Transform3D& = default ;
149181
182+ /* *
183+ * Helper compose function. The base implementation returns a nullptr.
184+ * Implementing derived classes should set the returned transform's source
185+ * to this->source() and the target to rhs->target().
186+ */
187+ [[nodiscard]] virtual auto compose_ (const Transform3D::Pointer& rhs) const
188+ -> Transform3D::Pointer;
189+
150190 /* * On-disk metadata type */
151191 using Metadata = nlohmann::ordered_json;
192+ /* * Serialize the transform to metadata */
193+ static auto Serialize (const Pointer& transform) -> Metadata;
194+ /* * Deserialize the transform from metadata */
195+ static auto Deserialize (const Metadata& meta) -> Pointer;
152196 /* * Serialize the derived class parameters */
153197 virtual void to_meta_ (Metadata& meta) = 0;
154198 /* * Deserialize the derived class parameters */
@@ -161,6 +205,18 @@ class Transform3D
161205 std::string tgt_;
162206};
163207
208+ /* *
209+ * @brief Compose transform convenience operator
210+ *
211+ * Same as Transform3D::Compose but only returns the composed transform. If
212+ * composition fails for any reason, will throw an exception.
213+ *
214+ * @throws std::invalid_argument if lhs or rhs are not composable
215+ * @throws std::runtime_error if transform composition failed
216+ */
217+ auto operator *(const Transform3D::Pointer& lhs, const Transform3D::Pointer& rhs)
218+ -> Transform3D::Pointer;
219+
164220/* *
165221 * @brief 3D affine transform
166222 *
@@ -169,7 +225,7 @@ class Transform3D
169225 * transform with the stored affine transform. For example, the following
170226 * transform will scale, rotate, and translate the 3D point, in that order:
171227 *
172- * @code
228+ * @code{.cpp}
173229 * auto tfm = AffineTransform::New();
174230 * // scale by 5
175231 * tfm->scale(5);
@@ -184,6 +240,9 @@ class Transform3D
184240class AffineTransform : public Transform3D
185241{
186242public:
243+ /* * @copydoc Transform3D::TYPE */
244+ static constexpr std::string_view TYPE{" AffineTransform" };
245+
187246 /* * Parameters type: 4x4 matrix */
188247 using Parameters = cv::Matx<double , 4 , 4 >;
189248
@@ -194,17 +253,17 @@ class AffineTransform : public Transform3D
194253 static auto New () -> Pointer;
195254
196255 /* * @copydoc Transform3D::type() */
197- [[nodiscard]] auto type () const -> std::string final ;
256+ [[nodiscard]] auto type () const -> std::string_view final ;
198257 /* * @copydoc Transform3D::clone() */
199258 [[nodiscard]] auto clone () const -> Transform3D::Pointer final ;
200259 /* * @copydoc Transform3D::invertible() */
201260 [[nodiscard]] auto invertible () const -> bool final ;
202261 /* * @copydoc Transform3D::invert() */
203262 [[nodiscard]] auto invert () const -> Transform3D::Pointer final ;
263+ /* * @copydoc Transform3D::composable() */
264+ [[nodiscard]] auto composable () const -> bool final ;
204265 /* * @copydoc Transform3D::reset() */
205266 void reset () final ;
206- /* * @copydoc Transform3D::clear() */
207- void clear () final ;
208267
209268 /* * @copydoc Transform3D::applyPoint() */
210269 [[nodiscard]] auto applyPoint (const cv::Vec3d& point) const
@@ -269,6 +328,161 @@ class AffineTransform : public Transform3D
269328 AffineTransform () = default ;
270329 /* * Current parameters */
271330 Parameters params_{cv::Matx<double , 4 , 4 >::eye ()};
331+ /* * @copydoc Transform3D::compose_() */
332+ [[nodiscard]] auto compose_ (const Transform3D::Pointer& rhs) const
333+ -> Transform3D::Pointer final ;
334+ /* * @copydoc Transform3D::to_meta_() */
335+ void to_meta_ (Metadata& meta) final ;
336+ /* * @copydoc Transform3D::from_meta_() */
337+ void from_meta_ (const Metadata& meta) final ;
338+ };
339+
340+ /* *
341+ * @brief Identity transform
342+ *
343+ * Identity transform that simply returns input parameters. Useful for
344+ * creating explicit mappings between a source and a target which share the
345+ * same coordinate space.
346+ *
347+ * @code{.cpp}
348+ * auto tfm = IdentityTransform::New();
349+ * auto pt = tfm->applyPoint({0, 1, 0}); // {0, 1, 0}
350+ * @endcode
351+ */
352+ class IdentityTransform : public Transform3D
353+ {
354+ public:
355+ /* * @copydoc Transform3D::TYPE */
356+ static constexpr std::string_view TYPE{" IdentityTransform" };
357+
358+ /* * Pointer type */
359+ using Pointer = std::shared_ptr<IdentityTransform>;
360+
361+ /* * @brief Create a new IdentityTransform */
362+ static auto New () -> Pointer;
363+
364+ /* * @copydoc Transform3D::type() */
365+ [[nodiscard]] auto type () const -> std::string_view final ;
366+ /* * @copydoc Transform3D::clone() */
367+ [[nodiscard]] auto clone () const -> Transform3D::Pointer final ;
368+ /* * @copydoc Transform3D::invertible() */
369+ [[nodiscard]] auto invertible () const -> bool final ;
370+ /* * @copydoc Transform3D::invert() */
371+ [[nodiscard]] auto invert () const -> Transform3D::Pointer final ;
372+ /* * @copydoc Transform3D::composable() */
373+ [[nodiscard]] auto composable () const -> bool final ;
374+ /* * @copydoc Transform3D::reset() */
375+ void reset () final ;
376+
377+ /* * @copydoc Transform3D::applyPoint() */
378+ [[nodiscard]] auto applyPoint (const cv::Vec3d& point) const
379+ -> cv::Vec3d final ;
380+ /* * @copydoc Transform3D::applyVector() */
381+ [[nodiscard]] auto applyVector (const cv::Vec3d& vector) const
382+ -> cv::Vec3d final ;
383+
384+ private:
385+ /* * Don't allow construction on the stack */
386+ IdentityTransform () = default ;
387+ /* * @copydoc Transform3D::compose_() */
388+ [[nodiscard]] auto compose_ (const Transform3D::Pointer& rhs) const
389+ -> Transform3D::Pointer final ;
390+ /* * @copydoc Transform3D::to_meta_() */
391+ void to_meta_ (Metadata& meta) final ;
392+ /* * @copydoc Transform3D::from_meta_() */
393+ void from_meta_ (const Metadata& meta) final ;
394+ };
395+
396+ /* *
397+ * @brief Collection of transforms
398+ *
399+ * A convenience class which holds a list of transforms. When transforming
400+ * points and vectors, each transform is applied sequentially to the input.
401+ *
402+ * @code{.cpp}
403+ * // New transform
404+ * auto tfm = CompositeTransform::New();
405+ *
406+ * // Add some transforms
407+ * auto t = AffineTransform::New();
408+ * t->translate(1, 2, 3);
409+ * tfm->push_back(t);
410+ * t->reset();
411+ * t->scale(4);
412+ * tfm->push_back(t);
413+ *
414+ * // Apply all transforms to an input
415+ * auto pt = tfm->applyPoint({0, 1, 0}); // {4, 12, 12}
416+ * @endcode
417+ *
418+ * It can often be preferable, for both performance and numerical stability, to
419+ * simplify all adjacent, composable transforms (e.g. AffineTransform,
420+ * IdentityTransform) into a single transform.
421+ *
422+ * @code{.cpp}
423+ * // Add some composable transforms
424+ * tfm->push_back(AffineTransform::New());
425+ * tfm->push_back(IdentityTransform::New());
426+ * tfm->push_back(AffineTransform::New());
427+ * tfm->size(); // 3
428+ *
429+ * // Simplify the transform
430+ * tfm->simplify();
431+ * tfm->size(); // 1
432+ * @endcode
433+ */
434+ class CompositeTransform : public Transform3D
435+ {
436+ public:
437+ /* * @copydoc Transform3D::TYPE */
438+ static constexpr std::string_view TYPE{" CompositeTransform" };
439+
440+ /* * Pointer type */
441+ using Pointer = std::shared_ptr<CompositeTransform>;
442+
443+ /* * @brief Create a new CompositeTransform */
444+ static auto New () -> Pointer;
445+
446+ /* * @copydoc Transform3D::type() */
447+ [[nodiscard]] auto type () const -> std::string_view final ;
448+ /* * @copydoc Transform3D::clone() */
449+ [[nodiscard]] auto clone () const -> Transform3D::Pointer final ;
450+ /* * @copydoc Transform3D::reset() */
451+ void reset () final ;
452+
453+ /* * @copydoc Transform3D::applyPoint() */
454+ [[nodiscard]] auto applyPoint (const cv::Vec3d& point) const
455+ -> cv::Vec3d final ;
456+ /* * @copydoc Transform3D::applyVector() */
457+ [[nodiscard]] auto applyVector (const cv::Vec3d& vector) const
458+ -> cv::Vec3d final ;
459+
460+ /* *
461+ * @brief Add a transform to the end of the composite transform stack
462+ *
463+ * The transform is cloned before being added to the transform stack. If
464+ * the transform is also a CompositeTransform, its transform stack is
465+ * expanded and copied to the end of this transform's stack.
466+ */
467+ void push_back (const Transform3D::Pointer& t);
468+
469+ /* * @brief Get the number of transforms in the composite transform */
470+ [[nodiscard]] auto size () const noexcept -> std::size_t;
471+
472+ /* *
473+ * @brief Compose all composable transforms
474+ *
475+ * Simplifies the transform by composing all adjacent, composable
476+ * transforms in the composite transform list. This can lead to better
477+ * runtime performance and numerical stability for the apply functions.
478+ */
479+ void simplify ();
480+
481+ private:
482+ /* * Don't allow construction on the stack */
483+ CompositeTransform () = default ;
484+ /* * Transform list */
485+ std::vector<Transform3D::Pointer> tfms_;
272486 /* * @copydoc Transform3D::to_meta_() */
273487 void to_meta_ (Metadata& meta) final ;
274488 /* * @copydoc Transform3D::from_meta_() */
0 commit comments