From 73e69fa6dc1e2064fdd00bd03601c4fb52fff2dc Mon Sep 17 00:00:00 2001 From: Kenshi Takayama Date: Fri, 5 Apr 2024 15:36:54 +0900 Subject: [PATCH] transform: accept 4x4 matrix --- README.md | 23 ++++++++---- src/cmd/transform.cpp | 84 ++++++++++++++++++++++++++++++------------- src/main.cpp | 27 +++++++++----- test/cmd.cpp | 57 ++++++++++++++++++++++++++++- 4 files changed, 151 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 46a582b..d3af2d3 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,9 @@ $ hairutil --help smooth Smooth strands stats Generate statistics subsample Subsample strands - transform Transform strand points + transform Transform strand points, either by one + of scale/translate/rotate, or by full + 4x4 matrix Common options: -i[PATH], --input-file=[PATH] (REQUIRED) Input file @@ -204,16 +206,23 @@ hairutil subsample -i test/data/Bangs_100.bin -o ply --indices 65,32,4,36,0 $ hairutil transform --help hairutil transform {OPTIONS} - Transform strand points + Transform strand points, either by one of scale/translate/rotate, or by full + 4x4 matrix OPTIONS: - -s[R], --scale=[R] Uniform scaling factor [1.0] - -t[R,R,R], --translate=[R,R,R] Comma-separated 3D vector for - translation [0,0,0] - -R[R,R,R,R,R,R,R,R,R], + -s[R or R,R,R], --scale=[R or + R,R,R] Scaling factor; either a single number + of comma-separated 3-tuple for + non-uniform scaling + -t[R,R,R], --translate=[R,R,R] Comma-separated 3-vector for translation + -r[R,R,R,R,R,R,R,R,R], --rotate=[R,R,R,R,R,R,R,R,R] Comma-separated row-major 3x3 matrix for - rotation [1,0,0,0,1,0,0,0,1] + rotation + -f[R,R,R,R,R,R,R,R,R,R,R,R,R,R,R,R], + --full=[R,R,R,R,R,R,R,R,R,R,R,R,R,R,R,R] + Comma-separated row-major 4x4 matrix for + full transform ``` Example: ``` diff --git a/src/cmd/transform.cpp b/src/cmd/transform.cpp index 7dc06a8..f9c9919 100644 --- a/src/cmd/transform.cpp +++ b/src/cmd/transform.cpp @@ -5,41 +5,76 @@ using namespace Eigen; namespace { struct { -float s; -Vector3f t; -Matrix3f R; +std::string s; +std::string t; +std::string r; +std::string f; +Matrix4f M = Matrix4f::Identity(); } param; } void cmd::parse::transform(args::Subparser &parser) { - args::ValueFlag scale(parser, "R", "Uniform scaling factor [1.0]", {"scale", 's'}, 1.0f); - args::ValueFlag translate(parser, "R,R,R", "Comma-separated 3D vector for translation [0,0,0]", {"translate", 't'}, "0,0,0"); - args::ValueFlag rotate(parser, "R,R,R,R,R,R,R,R,R", "Comma-separated row-major 3x3 matrix for rotation [1,0,0,0,1,0,0,0,1]", {"rotate", 'R'}, "1,0,0,0,1,0,0,0,1"); + args::ValueFlag scale(parser, "R or R,R,R", "Scaling factor; either a single number of comma-separated 3-tuple for non-uniform scaling", {"scale", 's'}, ""); + args::ValueFlag translate(parser, "R,R,R", "Comma-separated 3-vector for translation", {"translate", 't'}, ""); + args::ValueFlag rotate(parser, "R,R,R,R,R,R,R,R,R", "Comma-separated row-major 3x3 matrix for rotation", {"rotate", 'r'}, ""); + args::ValueFlag full(parser, "R,R,R,R,R,R,R,R,R,R,R,R,R,R,R,R", "Comma-separated row-major 4x4 matrix for full transform", {"full", 'f'}, ""); parser.Parse(); globals::cmd_exec = cmd::exec::transform; globals::output_file = [](){ - std::string t_str = fmt::format("{}", ::param.t.transpose()); - t_str = util::squash_underscores(util::replace_space_with_underscore(util::trim_whitespaces(t_str))); - - const Matrix3f Rt = ::param.R.transpose(); - std::string R_str = fmt::format("{}", Map>(Rt.data())); - R_str = util::squash_underscores(util::replace_space_with_underscore(util::trim_whitespaces(R_str))); - - return fmt::format("{}_tfm_s_{}_t_{}_R_{}.{}", globals::input_file_wo_ext, ::param.s, t_str, R_str, globals::output_ext); + if (!param.s.empty()) + return fmt::format("{}_tfm_s_{}.{}", globals::input_file_wo_ext, param.s, globals::output_ext); + if (!param.t.empty()) + return fmt::format("{}_tfm_t_{}.{}", globals::input_file_wo_ext, param.t, globals::output_ext); + if (!param.r.empty()) + return fmt::format("{}_tfm_r_{}.{}", globals::input_file_wo_ext, param.r, globals::output_ext); + return fmt::format("{}_tfm_f_{}.{}", globals::input_file_wo_ext, param.f, globals::output_ext); + }; + globals::check_error = [](){ + if (param.s.empty() + param.t.empty() + param.r.empty() + param.f.empty() != 3) { + throw std::runtime_error("Exactly one of --scale, --translate, --rotate, or --full must be specified"); + } }; ::param = {}; ::param.s = *scale; - const std::vector translate_vec = util::parse_comma_separated_values(*translate); - if (translate_vec.size() != 3) { - throw std::runtime_error(fmt::format("Invalid translation vector: {}", *translate)); + ::param.t = *translate; + ::param.r = *rotate; + ::param.f = *full; + + if (!::param.s.empty()) { + const std::vector scale_vec = util::parse_comma_separated_values(*scale); + if (scale_vec.size() == 1) { + ::param.M.block<3, 3>(0, 0) = scale_vec[0] * Matrix3f::Identity(); + } else if (scale_vec.size() == 3) { + ::param.M.block<3, 3>(0, 0) = Map(scale_vec.data()).asDiagonal(); + } else { + throw std::runtime_error(fmt::format("Invalid scaling factor: {}", *scale)); + } + } + + if (!::param.t.empty()) { + const std::vector translate_vec = util::parse_comma_separated_values(*translate); + if (translate_vec.size() != 3) { + throw std::runtime_error(fmt::format("Invalid translation vector: {}", *translate)); + } + ::param.M.block<3, 1>(0, 3) = Map(translate_vec.data()); } - ::param.t = Map(translate_vec.data()); - const std::vector rotate_vec = util::parse_comma_separated_values(*rotate); - if (rotate_vec.size() != 9) { - throw std::runtime_error(fmt::format("Invalid rotation matrix: {}", *rotate)); + + if (!::param.r.empty()) { + const std::vector rotate_vec = util::parse_comma_separated_values(*rotate); + if (rotate_vec.size() != 9) { + throw std::runtime_error(fmt::format("Invalid rotation matrix: {}", *rotate)); + } + ::param.M.block<3, 3>(0, 0) = Map(rotate_vec.data()).transpose(); + } + + if (!::param.f.empty()) { + const std::vector full_vec = util::parse_comma_separated_values(*full); + if (full_vec.size() != 16) { + throw std::runtime_error(fmt::format("Invalid transformation matrix: {}", *full)); + } + ::param.M = Map(full_vec.data()).transpose(); } - ::param.R = Map(rotate_vec.data()).transpose(); } std::shared_ptr cmd::exec::transform(std::shared_ptr hairfile_in) { @@ -49,8 +84,9 @@ std::shared_ptr cmd::exec::transform(std::shared_ptr hai for (unsigned int j = 0; j < segment_count + 1; ++j) { Map point(hairfile_in->GetPointsArray() + 3 * (offset + j)); - const Vector3f point_new = ::param.s * ::param.R * point + ::param.t; - point = point_new; + const Vector4f hpoint = (Vector4f() << point, 1).finished(); + const Vector4f hpoint_new = ::param.M * hpoint; + point = hpoint_new.head(3) / hpoint_new(3); } offset += segment_count + 1; } diff --git a/src/main.cpp b/src/main.cpp index 56553f1..9b327ef 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -31,7 +31,7 @@ int main(int argc, const char **argv) args::Command cmd_smooth(grp_commands, "smooth", "Smooth strands", cmd::parse::smooth); args::Command cmd_stats(grp_commands, "stats", "Generate statistics", cmd::parse::stats); args::Command cmd_subsample(grp_commands, "subsample", "Subsample strands", cmd::parse::subsample); - args::Command cmd_transform(grp_commands, "transform", "Transform strand points", cmd::parse::transform); + args::Command cmd_transform(grp_commands, "transform", "Transform strand points, either by one of scale/translate/rotate, or by full 4x4 matrix", cmd::parse::transform); args::Group grp_globals("Common options:"); args::ValueFlag globals_input_file(grp_globals, "PATH", "(REQUIRED) Input file", {'i', "input-file"}, args::Options::Required); @@ -146,11 +146,22 @@ int main(int argc, const char **argv) // Get input file name without extension globals::input_file_wo_ext = globals::input_file.substr(0, globals::input_file.find_last_of(".")); - // Check if the output file already exists - if (!globals::overwrite && globals::output_file && std::filesystem::exists(globals::output_file())) { - spdlog::error("Output file already exists: {}", globals::output_file()); - spdlog::error("Use --overwrite to overwrite the file"); - return 1; + // Check output filename validity and existence + std::string output_file; + if (globals::output_file) { + output_file = globals::output_file(); + + // Truncate if too long + if (output_file.length() > 230) { + spdlog::warn("Output file path is too long ({}), truncating", output_file.length()); + output_file = output_file.substr(0, 230) + "." + globals::output_ext; + } + + if (!globals::overwrite && std::filesystem::exists(output_file)) { + spdlog::error("Output file already exists: {}", output_file); + spdlog::error("Use --overwrite to overwrite the file"); + return 1; + } } try @@ -175,8 +186,8 @@ int main(int argc, const char **argv) auto hairfile_out = globals::cmd_exec(hairfile_in); if (hairfile_out) { - spdlog::info("Saving to {} ...", globals::output_file()); - globals::save_func(globals::output_file(), hairfile_out); + spdlog::info("Saving to {} ...", output_file); + globals::save_func(output_file, hairfile_out); } } catch (const std::exception &e) diff --git a/test/cmd.cpp b/test/cmd.cpp index f27a886..ad532ab 100644 --- a/test/cmd.cpp +++ b/test/cmd.cpp @@ -270,7 +270,7 @@ TEST(cmd_subsample, indices_single) { EXPECT_EQ(test_main(args.size(), args.data()), 0); } -TEST(cmd_transform, bin_to_ply) { +TEST(cmd_transform, invalid_args) { std::vector args = { "test_cmd", "transform", @@ -279,6 +279,44 @@ TEST(cmd_transform, bin_to_ply) { "--overwrite", "--scale", "1.2", "--translate", "12.3,45.6,78.9", + }; + globals::clear(); + EXPECT_EQ(test_main(args.size(), args.data()), 1); +} + +TEST(cmd_transform, scale) { + std::vector args = { + "test_cmd", + "transform", + "-i", TEST_DATA_DIR "/Bangs_100.bin", + "-o", "ply", + "--overwrite", + "--scale", "1.2", + }; + globals::clear(); + EXPECT_EQ(test_main(args.size(), args.data()), 0); +} + +TEST(cmd_transform, translate) { + std::vector args = { + "test_cmd", + "transform", + "-i", TEST_DATA_DIR "/Bangs_100.bin", + "-o", "ply", + "--overwrite", + "--translate", "12.3,45.6,78.9", + }; + globals::clear(); + EXPECT_EQ(test_main(args.size(), args.data()), 0); +} + +TEST(cmd_transform, rotate) { + std::vector args = { + "test_cmd", + "transform", + "-i", TEST_DATA_DIR "/Bangs_100.bin", + "-o", "ply", + "--overwrite", "--rotate", "0.407903582,-0.656201959,0.634833455," "0.838385462,0.54454118,0.0241773129," @@ -288,6 +326,23 @@ TEST(cmd_transform, bin_to_ply) { EXPECT_EQ(test_main(args.size(), args.data()), 0); } +TEST(cmd_transform, full) { + std::vector args = { + "test_cmd", + "transform", + "-i", TEST_DATA_DIR "/Bangs_100.bin", + "-o", "ply", + "--overwrite", + "--full", + "0.407903582,-0.656201959,0.634833455,12.3," + "0.838385462,0.54454118,0.0241773129,45.6," + "-0.361558199,0.52237314,0.77227056,78.9," + "0,0,0,1" + }; + globals::clear(); + EXPECT_EQ(test_main(args.size(), args.data()), 0); +} + int main(int argc, char **argv) { spdlog::set_level(spdlog::level::trace); testing::InitGoogleTest(&argc, argv);