Commit ddcce250 authored by Richard Torenvliet's avatar Richard Torenvliet

Add tests for projection error - not realy tests but accuracy measure tests

parent df1e1286
......@@ -42,19 +42,16 @@ set(CATCH_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/Catch/include CACHE INTERNAL
add_library(Catch INTERFACE)
target_include_directories(Catch INTERFACE ${CATCH_INCLUDE_DIR})
add_executable (fitting-test fitting/fitting_test.cpp)
target_link_libraries(fitting-test Catch ${OPENCV_LIBS} ${BOOST_LIBRARIES})
set(${EOS_TESTS_DIRS} ${CMAKE_SOURCE_DIR}/tests/fitting "Paths to test include directories")
add_test (NAME fitting-test COMMAND fitting-test)
install(TARGETS fitting-test DESTINATION tests)
add_executable (main-tests main_tests.cpp)
target_link_libraries(main-tests Catch ${EOS_TESTS_DIRS} ${OpenCV_LIBS} ${Boost_LIBRARIES})
install(TARGETS main-tests DESTINATION tests)
install(DIRECTORY ${CMAKE_SOURCE_DIR}/tests/data DESTINATION tests)
# Simple model fitting (orthographic camera & shape to landmarks) example:
#add_executable(fit-model-simple fit-model-simple.cpp)
#add_executable(fitting-tests )
#target_link_libraries(fit-model-simple ${OpenCV_LIBS} ${Boost_LIBRARIES})
# install target:
#install(TARGETS fit-model-simple DESTINATION bin)
#install(DIRECTORY ${CMAKE_SOURCE_DIR}/examples/data DESTINATION bin)
# add tests
enable_testing()
add_test(NAME fitting-test COMMAND fitting-test)
add_test(NAME linear-shape-fitting-test COMMAND linear-shape-fitting-test)
version: 1
n_points: 68
{
611.284152 272.773913
607.899810 304.319120
613.094672 339.255590
622.808910 372.005502
632.669412 404.678824
643.511765 432.387059
651.944706 460.095294
665.273062 490.435795
695.836418 505.268304
732.882854 507.601124
771.140919 496.682178
814.580000 468.528235
852.378512 442.776782
879.780380 411.071469
894.313442 365.675605
905.385571 315.920470
912.766990 265.622269
608.920127 214.000560
624.612439 205.921886
641.092550 213.028614
656.643518 218.496370
675.118172 229.895972
714.507903 232.709448
746.296501 221.279247
780.463683 216.098001
814.843700 219.577695
843.144930 231.216340
691.116694 258.293884
686.687460 284.649159
678.837382 314.952208
671.575170 341.518858
662.787059 357.695294
672.577890 363.872007
684.099935 368.436761
701.177972 366.977129
716.657204 364.432781
629.308705 253.419965
641.102353 239.634118
656.763529 242.043529
675.896682 261.234513
657.320778 266.311140
639.427321 264.662791
746.963287 268.769913
763.151112 249.645230
792.895294 251.681176
810.965882 264.932941
793.647370 274.870034
770.089168 277.392535
650.466760 402.919129
662.802218 396.521655
674.655327 393.178631
685.548727 399.199281
699.727862 394.238444
720.617079 403.327949
762.033016 412.238881
727.622851 431.747041
706.539636 437.383098
691.346572 438.212545
679.045609 436.837854
665.204535 428.934293
657.444159 406.740225
677.532790 409.255983
688.027122 410.142694
701.031914 411.571227
750.088005 413.800864
701.031914 411.571227
688.027122 410.142694
677.532790 409.255983
}
\ No newline at end of file
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
TEST_CASE("stupid/1=1", "Prove that one equals two" ){
int one = 1;
REQUIRE( one == 1 );
}
#include "catch.hpp"
#include "glm/ext.hpp"
#include "glm/gtc/matrix_transform.hpp"
#include "glm/gtx/transform.hpp"
#include "eos/core/Landmark.hpp"
#include "eos/core/LandmarkMapper.hpp"
#include "eos/fitting/orthographic_camera_estimation_linear.hpp"
#include "eos/fitting/RenderingParameters.hpp"
#include "eos/fitting/linear_shape_fitting.hpp"
#include "eos/render/utils.hpp"
#include "eos/render/texture_extraction.hpp"
#include "eos/render/render.hpp"
#include "eos/render/detail/render_detail.hpp"
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "boost/program_options.hpp"
#include "boost/filesystem.hpp"
#include <vector>
#include <iostream>
#include <fstream>
#include <tuple>
#include <random>
#include <cmath>
#include <numeric>
using namespace eos;
namespace po = boost::program_options;
namespace fs = boost::filesystem;
using eos::core::Landmark;
using eos::core::LandmarkCollection;
using cv::Mat;
using cv::Vec2f;
using cv::Vec3f;
using cv::Vec4f;
using std::cout;
using std::endl;
using std::vector;
using std::string;
using std::tuple;
/**
* Reads an ibug .pts landmark file and returns an ordered vector with
* the 68 2D landmark coordinates.
*
* @param[in] filename Path to a .pts file.
* @return An ordered vector with the 68 ibug landmarks.
*/
LandmarkCollection<cv::Vec2f> read_pts_landmarks(std::string filename)
{
using std::getline;
using cv::Vec2f;
using std::string;
LandmarkCollection<Vec2f> landmarks;
landmarks.reserve(68);
std::ifstream file(filename);
if (!file.is_open()) {
throw std::runtime_error(string("Could not open landmark file: " + filename));
}
string line;
// Skip the first 3 lines, they're header lines:
getline(file, line); // 'version: 1'
getline(file, line); // 'n_points : 68'
getline(file, line); // '{'
int ibugId = 1;
while (getline(file, line))
{
if (line == "}") { // end of the file
break;
}
std::stringstream lineStream(line);
Landmark<Vec2f> landmark;
landmark.name = std::to_string(ibugId);
if (!(lineStream >> landmark.coordinates[0] >> landmark.coordinates[1])) {
throw std::runtime_error(string("Landmark format error while parsing the line: " + line));
}
// From the iBug website:
// "Please note that the re-annotated data for this challenge are saved in the Matlab convention of 1 being
// the first index, i.e. the coordinates of the top left pixel in an image are x=1, y=1."
// ==> So we shift every point by 1:
landmark.coordinates[0] -= 1.0f;
landmark.coordinates[1] -= 1.0f;
landmarks.emplace_back(landmark);
++ibugId;
}
return landmarks;
};
morphablemodel::MorphableModel loadTestModel() {
return morphablemodel::load_model("share/sfm_shape_3448.bin");
}
/**
* Loads test data. Returns
* @param landmarks
* @param landmark_mapper
* @return
*/
std::tuple<morphablemodel::MorphableModel, vector<Vec4f>, vector<int>, vector<Vec2f>> loadTestData(
LandmarkCollection<cv::Vec2f> landmarks, core::LandmarkMapper landmark_mapper) {
morphablemodel::MorphableModel morphable_model = loadTestModel();
// These will be the final 2D and 3D points used for the fitting:
vector<Vec4f> model_points; // the points in the 3D shape model
vector<int> vertex_indices; // their vertex indices
vector<Vec2f> image_points; // the corresponding 2D landmark points
// Sub-select all the landmarks which we have a mapping for (i.e. that are defined in the 3DMM):
for (int i = 0; i < landmarks.size(); ++i) {
auto converted_name = landmark_mapper.convert(landmarks[i].name);
// no mapping defined for the current landmark
if (!converted_name) {
continue;
}
int vertex_idx = std::stoi(converted_name.get());
Vec4f vertex = morphable_model.get_shape_model().get_mean_at_point(vertex_idx);
model_points.emplace_back(vertex);
vertex_indices.emplace_back(vertex_idx);
image_points.emplace_back(landmarks[i].coordinates);
}
return std::make_tuple(morphable_model, model_points, vertex_indices, image_points);
}
/**
* Helper function to calculate the euclidean distance between the landmark and a projected
* point. Nothing more than Pythogas.
*
* @param landmark
* @param vertex_screen_coords
* @return
*/
inline float euclidean_distance(cv::Vec2f landmark, cv::Mat vertex_screen_coords) {
float screen_x = vertex_screen_coords.at<float>(0, 0);
float screen_y = vertex_screen_coords.at<float>(1, 0);
// Calc squared differences, ready for use in Pythagoras.
float landmark_diff_x_sq = std::fabs(landmark[0] - screen_x) * std::fabs(landmark[0] - screen_x);
float landmark_diff_y_sq = std::fabs(landmark[1] - screen_y) * std::fabs(landmark[0] - screen_x);
return std::sqrt(landmark_diff_x_sq + landmark_diff_y_sq);
}
TEST_CASE("Test ortographic projection", "[projection]" ){
// ======== begin setup ============
Mat image = cv::imread("tests/data/image_0010.png");
LandmarkCollection<cv::Vec2f> landmarks;
landmarks = read_pts_landmarks("tests/data/image_0010.pts");
core::LandmarkMapper landmark_mapper = core::LandmarkMapper("share/ibug2did.txt");
vector<Vec4f> model_points; // the points in the 3D shape model
vector<int> vertex_indices; // their vertex indices
vector<Vec2f> image_points; // the corresponding 2D landmark points
morphablemodel::MorphableModel morphable_model;
std::tie(morphable_model, model_points, vertex_indices, image_points) = loadTestData(landmarks, landmark_mapper);
// Estimate the camera (pose) from the 2D - 3D point correspondences
fitting::ScaledOrthoProjectionParameters pose = fitting::estimate_orthographic_projection_linear(
image_points, model_points, true, image.rows
);
fitting::RenderingParameters rendering_params(pose, image.cols, image.rows);
// Estimate the shape coefficients by fitting the shape to the landmarks:
Mat affine_from_ortho = fitting::get_3x4_affine_camera_matrix(rendering_params, image.cols, image.rows);
vector<float> fitted_coeffs = fitting::fit_shape_to_landmarks_linear(
morphable_model, affine_from_ortho, image_points, vertex_indices
);
// Obtain the full mesh with the estimated coefficients:
render::Mesh mesh = morphable_model.draw_sample(fitted_coeffs, vector<float>());
// ======== end setup ============
SECTION("Landmark projection error") {
vector<float> total_error;
// Sub-select all the landmarks which we have a mapping for (i.e. that are defined in the 3DMM):
for (int i = 0; i < landmarks.size(); ++i) {
auto converted_name = landmark_mapper.convert(landmarks[i].name);
// no mapping defined for the current landmark
if (!converted_name) {
continue;
}
int vertex_idx = std::stoi(converted_name.get());
// The vertex_idx should be around the value of the original coordinates after we have
// projected it with the affine_from_ortho that is obtained earlier.
Mat vertex_screen_coords = affine_from_ortho *
Mat(cv::Vec4f(
mesh.vertices[vertex_idx].x,
mesh.vertices[vertex_idx].y,
mesh.vertices[vertex_idx].z,
mesh.vertices[vertex_idx].w)
);
// using euclidean distance here, but should look at other ways too.
total_error.push_back(euclidean_distance(landmarks[i].coordinates, vertex_screen_coords));
}
// Caculate mean error and stddev.
float accum = 0.0;
float mean_error = std::accumulate(total_error.begin(), total_error.end(), 0) / landmarks.size();
// cacl. standardeviation
std::for_each (std::begin(total_error), std::end(total_error), [&](const float d) {
accum += (d - mean_error) * (d - mean_error);
});
float stddev = std::sqrt(accum / (total_error.size() - 1));
CAPTURE(mean_error);
CAPTURE(stddev);
// TODO: make better requirements / tests for these values.
// These are just based on the current output of the tests, it however make sure that we do
// not go over these values while altering eos code.
REQUIRE(mean_error < 4.0f);
REQUIRE(stddev < 5.0f);
}
}
// Example file, show to use Catch with multiple files through one main_tests file.
#include "catch.hpp"
TEST_CASE("Test 1 == 1", "[fitting]") {
REQUIRE(1 == 1);
}
\ No newline at end of file
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
// Include all tests here and don't forget to alter the CMakeLists.txt in the tests folder,
// that means use add_test to add the a new test file.
#include "fitting/fitting_test.hpp"
#include "fitting/linear_shape_fitting_test.hpp"
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment