Skip to content

Commit

Permalink
transform: accept 4x4 matrix
Browse files Browse the repository at this point in the history
  • Loading branch information
kenshi84 committed Apr 5, 2024
1 parent 7a7a314 commit 73e69fa
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 40 deletions.
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
```
Expand Down
84 changes: 60 additions & 24 deletions src/cmd/transform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<float> scale(parser, "R", "Uniform scaling factor [1.0]", {"scale", 's'}, 1.0f);
args::ValueFlag<std::string> translate(parser, "R,R,R", "Comma-separated 3D vector for translation [0,0,0]", {"translate", 't'}, "0,0,0");
args::ValueFlag<std::string> 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<std::string> 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<std::string> translate(parser, "R,R,R", "Comma-separated 3-vector for translation", {"translate", 't'}, "");
args::ValueFlag<std::string> rotate(parser, "R,R,R,R,R,R,R,R,R", "Comma-separated row-major 3x3 matrix for rotation", {"rotate", 'r'}, "");
args::ValueFlag<std::string> 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<const Matrix<float, 1, 9>>(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<float> translate_vec = util::parse_comma_separated_values<float>(*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<float> scale_vec = util::parse_comma_separated_values<float>(*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<const Vector3f>(scale_vec.data()).asDiagonal();
} else {
throw std::runtime_error(fmt::format("Invalid scaling factor: {}", *scale));
}
}

if (!::param.t.empty()) {
const std::vector<float> translate_vec = util::parse_comma_separated_values<float>(*translate);
if (translate_vec.size() != 3) {
throw std::runtime_error(fmt::format("Invalid translation vector: {}", *translate));
}
::param.M.block<3, 1>(0, 3) = Map<const Vector3f>(translate_vec.data());
}
::param.t = Map<const Vector3f>(translate_vec.data());
const std::vector<float> rotate_vec = util::parse_comma_separated_values<float>(*rotate);
if (rotate_vec.size() != 9) {
throw std::runtime_error(fmt::format("Invalid rotation matrix: {}", *rotate));

if (!::param.r.empty()) {
const std::vector<float> rotate_vec = util::parse_comma_separated_values<float>(*rotate);
if (rotate_vec.size() != 9) {
throw std::runtime_error(fmt::format("Invalid rotation matrix: {}", *rotate));
}
::param.M.block<3, 3>(0, 0) = Map<const Matrix3f>(rotate_vec.data()).transpose();
}

if (!::param.f.empty()) {
const std::vector<float> full_vec = util::parse_comma_separated_values<float>(*full);
if (full_vec.size() != 16) {
throw std::runtime_error(fmt::format("Invalid transformation matrix: {}", *full));
}
::param.M = Map<const Matrix4f>(full_vec.data()).transpose();
}
::param.R = Map<const Matrix3f>(rotate_vec.data()).transpose();
}

std::shared_ptr<cyHairFile> cmd::exec::transform(std::shared_ptr<cyHairFile> hairfile_in) {
Expand All @@ -49,8 +84,9 @@ std::shared_ptr<cyHairFile> cmd::exec::transform(std::shared_ptr<cyHairFile> hai

for (unsigned int j = 0; j < segment_count + 1; ++j) {
Map<Vector3f> 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;
}
Expand Down
27 changes: 19 additions & 8 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> globals_input_file(grp_globals, "PATH", "(REQUIRED) Input file", {'i', "input-file"}, args::Options::Required);
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
57 changes: 56 additions & 1 deletion test/cmd.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<const char*> args = {
"test_cmd",
"transform",
Expand All @@ -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<const char*> 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<const char*> 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<const char*> 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,"
Expand All @@ -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<const char*> 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);
Expand Down

0 comments on commit 73e69fa

Please sign in to comment.