Commit 949c8db1 authored by Richard Torenvliet's avatar Richard Torenvliet

First version testing the multi-frame chain with bufferedVideoIterator

parent 6690296f
......@@ -84,6 +84,29 @@ namespace eos {
return landmarks;
}
/**
* Read strings from a given file and return them as a vector of strings.
*
* @param filename
* @return
*/
std::vector<std::string> file_to_string_vector(std::string filename) {
std::ifstream file(filename);
if (!file.is_open()) {
throw std::runtime_error(string("Could not open annotation list file: " + filename));
}
string line;
std::vector<std::string> output;
while (getline(file, line)) {
std::cout << line << std::endl;
output.push_back(line);
}
return output;
}
/**
* Helper function, gathers matching model_points with given landmarks and LandmarkMapper.
*
......@@ -115,22 +138,32 @@ namespace eos {
}
/**
* Load annotations, return all annotations as image points (vectors of Vec2f).
* Load annotations, return all annotations as a vector of LandmarkCollection (vectors of Vec2f).
* Supports either a given template, any cv::Vec2f / Eigen::Vector2f will work.
*
* Returns the annotations as a list of files, useful for later use if read_from_file flag is set to true.
* This flag will read the filepaths from the given annotation file, probably depends on the command line args.
*
* @tparam vec2f
* @param annotations
* @param mappingsfile
* @throws std::runtime_error in case of faulty annotation file
* @return std::vector<std::vector<cv::Vec2f>> image_points in a vector of OpenCV float pairs (Vec2f).
* @param mapper
* @param morphable_model
* @param read_from_file
* @return
*/
template<typename vec2f, typename vec3f>
std::vector<core::LandmarkCollection<vec2f>> load_annotations(std::vector <std::string> annotations, fs::path mappingsfile) {
//std::vector <std::vector<vec2f>> image_points; // the corresponding 2D landmark points of all annotation files.
eos::core::LandmarkMapper landmark_mapper = eos::core::LandmarkMapper(mappingsfile);
template<typename vec2f>
std::pair <std::vector<core::LandmarkCollection<vec2f>>, std::vector<std::string>>
load_annotations(
std::vector<std::string>annotations,
core::LandmarkMapper mapper, morphablemodel::MorphableModel morphable_model, bool read_from_file = false) {
std::vector<core::LandmarkCollection<vec2f>> landmark_collection_list;
// These will be the final 2D and 3D points used for the fitting:
std::vector <vec3f> model_points; // the points in the 3D shape model
std::vector<int> vertex_indices; // their vertex indices
// load file names from one file.
std::vector<std::string> annotation_files;
if (read_from_file) {
annotations = file_to_string_vector(annotations[0]);
}
for (int i = 0; i < annotations.size(); i++) {
eos::core::LandmarkCollection <vec2f> landmarks;
......@@ -142,13 +175,17 @@ namespace eos {
throw std::runtime_error("Error reading the landmarks: " + annotations[i]);
}
core::LandmarkCollection<vec2f> image_points;
// Sub-select all the landmarks which we have a mapping for (i.e. that are defined in the 3DMM):
for (int j = 0; j < landmarks.size(); j++) {
landmark_collection_list.emplace_back(landmarks);
for (int i = 0; i < landmarks.size(); ++i) {
image_points.emplace_back(landmarks[i]);
}
landmark_collection_list.push_back(image_points);
}
return landmark_collection_list;
return std::make_pair(landmark_collection_list, annotations);
}
}
}
......
......@@ -464,6 +464,7 @@ inline std::pair<std::vector<core::Mesh>, std::vector<fitting::RenderingParamete
std::vector<float>& pca_shape_coefficients,
std::vector<std::vector<float>>& blendshape_coefficients,
std::vector<std::vector<cv::Vec2f>>& fitted_image_points) {
assert(blendshapes.size() > 0);
assert(num_iterations > 0); // Can we allow 0, for only the initial pose-fit?
assert(pca_shape_coefficients.size() <= morphable_model.get_shape_model().get_num_principal_components());
......@@ -475,7 +476,6 @@ inline std::pair<std::vector<core::Mesh>, std::vector<fitting::RenderingParamete
using Eigen::VectorXf;
using Eigen::MatrixXf;
if (!num_shape_coefficients_to_fit) {
num_shape_coefficients_to_fit = morphable_model.get_shape_model().get_num_principal_components();
}
......@@ -691,6 +691,26 @@ inline std::pair<std::vector<core::Mesh>, std::vector<fitting::RenderingParamete
);
};
inline std::pair<std::vector<core::Mesh>, std::vector<fitting::RenderingParameters>> fit_shape_and_pose_multi_2(
const morphablemodel::MorphableModel& morphable_model,
const std::vector<morphablemodel::Blendshape>& blendshapes,
const std::vector<core::LandmarkCollection<cv::Vec2f>>& landmarks,
const core::LandmarkMapper& landmark_mapper,
int image_width,
int image_height,
int num_images,
const morphablemodel::EdgeTopology& edge_topology,
const fitting::ContourLandmarks& contour_landmarks,
const fitting::ModelContour& model_contour,
int num_iterations,
boost::optional<int> num_shape_coefficients_to_fit,
float lambda,
boost::optional<fitting::RenderingParameters> initial_rendering_params,
std::vector<float>& pca_shape_coefficients,
std::vector<std::vector<float>>& blendshape_coefficients,
std::vector<std::vector<cv::Vec2f>>& fitted_image_points) {
}
} /* namespace fitting */
} /* namespace eos */
......
......@@ -43,7 +43,7 @@ using namespace std;
namespace eos {
namespace video {
// TODO: maybe move video iterator here.. or remove file.
// TODO: maybe move video iterator here.. or remove fil.
}
}
......
......@@ -58,9 +58,10 @@ public:
* @param frame
* @param score
*/
Keyframe(float score, cv::Mat frame) {
Keyframe(float score, cv::Mat frame, int frame_number) {
this->score = score;
this->frame = frame;
this->frame_number = frame_number;
}
/**
......@@ -78,6 +79,7 @@ public:
float score = 0.0f;
cv::Mat frame;
fitting::FittingResult fitting_result;
int frame_number;
};
/**
......@@ -319,19 +321,18 @@ public:
BufferedVideoIterator(std::string videoFilePath, int max_frames = 5, int min_frames = 4) {
std::ifstream file(videoFilePath);
std::cout << "video file path: " << videoFilePath << std::endl;
if (!file.is_open()) {
throw std::runtime_error("Error opening given file: " + videoFilePath);
}
cv::VideoCapture cap(videoFilePath); // open video file
cv::VideoCapture tmp_cap(videoFilePath); // open video file
if (!cap.isOpened()) { // check if we succeeded
if (!tmp_cap.isOpened()) { // check if we succeeded
throw std::runtime_error("Could not play video");
}
width = static_cast<int>(cap.get(CV_CAP_PROP_FRAME_HEIGHT));
height = static_cast<int>(cap.get(CV_CAP_PROP_FRAME_HEIGHT));
this->cap = tmp_cap;
this->max_frames = max_frames;
this->min_frames = min_frames;
}
......@@ -350,15 +351,22 @@ public:
cv::Mat frame;
cap >> frame;
if (frame.empty()) {
frame_buffer = next();
return frame_buffer;
}
// Pop if we exceeded max_frames.
if (n_frames > max_frames) {
frame_buffer.pop_front();
n_frames--;
}
float lap_score = static_cast<float>(variance_of_laplacian(frame));
if (lap_score < laplacian_threshold) {
frame_buffer.push_back(Keyframe(lap_score, frame));
total_frames_processed++;
frame_buffer.push_back(Keyframe(lap_score, frame, total_frames_processed));
}
n_frames++;
......@@ -379,11 +387,13 @@ private:
std::deque<Keyframe> frame_buffer;
// TODO: make set-able
long total_frames_processed = 1;
// total frames in buffer
long n_frames = 1;
// min frames to load at the start.
long min_frames = 3;
// keep max_frames into the buffer.
long max_frames = 5;
......@@ -394,6 +404,78 @@ private:
double laplacian_threshold = 10000000;
};
/**
* @brief Merges isomaps from a live video with a weighted averaging, based
* on the view angle of each vertex to the camera.
*
* An optional merge_threshold can be specified upon construction. Pixels with
* a view-angle above that threshold will be completely discarded. All pixels
* below the threshold are merged with a weighting based on its vertex view-angle.
* Assumes the isomaps to be 512x512.
*/
class WeightedIsomapAveraging
{
public:
/**
* @brief Constructs a new object that will hold the current averaged isomap and
* be able to add frames from a live video and merge them on-the-fly.
*
* The threshold means: Each triangle with a view angle smaller than the given angle will be used to merge.
* The default threshold (90°) means all triangles, as long as they're a little bit visible, are merged.
*
* @param[in] merge_threshold View-angle merge threshold, in degrees, from 0 to 90.
*/
WeightedIsomapAveraging(float merge_threshold = 90.0f)
{
assert(merge_threshold >= 0.f && merge_threshold <= 90.f);
visibility_counter = cv::Mat::zeros(512, 512, CV_32SC1);
merged_isomap = cv::Mat::zeros(512, 512, CV_32FC4);
// map 0° to 255, 90° to 0:
float alpha_thresh = (-255.f / 90.f) * merge_threshold + 255.f;
if (alpha_thresh < 0.f) // could maybe happen due to float inaccuracies / rounding?
alpha_thresh = 0.0f;
threshold = static_cast<unsigned char>(alpha_thresh);
};
/**
* @brief Merges the given new isomap with all previously processed isomaps.
*
* @param[in] isomap The new isomap to add.
* @return The merged isomap of all images processed so far, as 8UC4.
*/
cv::Mat add_and_merge(const cv::Mat& isomap)
{
// Merge isomaps, add the current to the already merged, pixel by pixel:
for (int r = 0; r < isomap.rows; ++r)
{
for (int c = 0; c < isomap.cols; ++c)
{
if (isomap.at<cv::Vec4b>(r, c)[3] <= threshold)
{
continue; // ignore this pixel, not visible in the extracted isomap of this current frame
}
// we're sure to have a visible pixel, merge it:
// merged_pixel = (old_average * visible_count + new_pixel) / (visible_count + 1)
merged_isomap.at<cv::Vec4f>(r, c)[0] = (merged_isomap.at<cv::Vec4f>(r, c)[0] * visibility_counter.at<int>(r, c) + isomap.at<cv::Vec4b>(r, c)[0]) / (visibility_counter.at<int>(r, c) + 1);
merged_isomap.at<cv::Vec4f>(r, c)[1] = (merged_isomap.at<cv::Vec4f>(r, c)[1] * visibility_counter.at<int>(r, c) + isomap.at<cv::Vec4b>(r, c)[1]) / (visibility_counter.at<int>(r, c) + 1);
merged_isomap.at<cv::Vec4f>(r, c)[2] = (merged_isomap.at<cv::Vec4f>(r, c)[2] * visibility_counter.at<int>(r, c) + isomap.at<cv::Vec4b>(r, c)[2]) / (visibility_counter.at<int>(r, c) + 1);
merged_isomap.at<cv::Vec4f>(r, c)[3] = 255; // as soon as we've seen the pixel visible once, we set it to visible.
++visibility_counter.at<int>(r, c);
}
}
cv::Mat merged_isomap_uchar;
merged_isomap.convertTo(merged_isomap_uchar, CV_8UC4);
return merged_isomap_uchar;
};
private:
cv::Mat visibility_counter;
cv::Mat merged_isomap;
unsigned char threshold;
};
} /* namespace video */
} /* namespace eos */
......
......@@ -17,6 +17,7 @@
* limitations under the License.
*/
#include "eos/core/landmark_utils.hpp"
#include "eos/render/render.hpp"
#include "eos/morphablemodel/morphablemodel.hpp"
#include "eos/morphablemodel/blendshape.hpp"
#include "eos/fitting/fitting.hpp"
......@@ -30,6 +31,7 @@
#include "boost/program_options.hpp"
#include "boost/filesystem.hpp"
#include "glm/gtx/string_cast.hpp"
#include <vector>
#include <iostream>
......@@ -41,6 +43,7 @@ namespace fs = boost::filesystem;
using eos::core::Landmark;
using eos::core::LandmarkCollection;
using eos::video::BufferedVideoIterator;
using eos::video::WeightedIsomapAveraging;
using cv::Mat;
using cv::Vec2f;
using cv::Vec3f;
......@@ -48,7 +51,6 @@ using cv::Vec4f;
using std::vector;
using std::string;
using namespace cv;
/**
......@@ -79,6 +81,119 @@ void draw_wireframe(cv::Mat image, const eos::core::Mesh& mesh, glm::mat4x4 mode
}
};
void evaluate_results(
std::deque<eos::video::Keyframe> key_frames,
std::vector<fitting::RenderingParameters> rendering_paramss,
std::vector<core::LandmarkCollection<cv::Vec2f>> landmark_list,
morphablemodel::MorphableModel morphable_model,
vector<core::Mesh> meshs,
std::vector<float> pca_shape_coefficients,
std::vector<std::vector<float>> blend_shape_coefficients,
std::vector<std::vector<cv::Vec2f>> fitted_image_points,
std::vector<std::string> annotations, int n_iter) {
WeightedIsomapAveraging isomap_averaging(60.f); // merge all triangles that are facing <60° towards the camera
Mat merged_isomap;
fs::path outputfilebase = annotations[0];
// The 3D head pose can be recovered as follows:
for (uint i = 0; i < key_frames.size(); ++i) {
float yaw_angle = glm::degrees(glm::yaw(rendering_paramss[i].get_rotation()));
int frame_number = key_frames[i].frame_number;
Mat frame = key_frames[i].frame;
int frame_width = frame.cols;
int frame_height = frame.rows;
// and similarly for pitch and roll.
// Extract the texture from the image using given mesh and camera parameters:
Mat affine_from_ortho = fitting::get_3x4_affine_camera_matrix(
rendering_paramss[i], frame_width, frame_height
);
// Draw the loaded landmarks:
Mat isomap = render::extract_texture(meshs[i], affine_from_ortho, frame);
Mat outimg = frame.clone();
std::cout << "processing:" << frame_number << std::endl;
for (auto&& lm : landmark_list[frame_number]) {
cv::rectangle(
outimg, cv::Point2f(lm.coordinates[0] - 2.0f, lm.coordinates[1] - 2.0f),
cv::Point2f(lm.coordinates[0], lm.coordinates[1] + 2.0f), { 255, 0, 0 }
);
}
// Draw the fitted mesh as wireframe, and save the image:
draw_wireframe(
outimg,
meshs[i],
rendering_paramss[i].get_modelview(),
rendering_paramss[i].get_projection(),
fitting::get_opencv_viewport(frame_width, frame_height)
);
fs::path outputfile = annotations[i];
outputfile.replace_extension(".annotated.png");
cv::imwrite(outputfile.string(), outimg);
// save frontal rendering with texture:
Mat frontal_rendering;
glm::mat4 modelview_frontal = glm::mat4( 1.0 );
core::Mesh neutral_expression = morphablemodel::sample_to_mesh(
morphable_model.get_shape_model().draw_sample(pca_shape_coefficients),
morphable_model.get_color_model().get_mean(),
morphable_model.get_shape_model().get_triangle_list(),
morphable_model.get_color_model().get_triangle_list(),
morphable_model.get_texture_coordinates()
);
std::tie(frontal_rendering, std::ignore) = eos::render::render(neutral_expression, modelview_frontal, glm::ortho(-130.0f, 130.0f, -130.0f, 130.0f), 256, 256, render::create_mipmapped_texture(isomap), true, false, false);
cv::imshow("frame", isomap);
outputfile.replace_extension(".frontal.png");
cv::imwrite(outputfile.string(), frontal_rendering);
outputfile.replace_extension("");
// And save the isomap:
if (!isomap.empty()) {
// And save the isomap:
outputfile.replace_extension(".isomap.png");
cv::imwrite(outputfile.string(), isomap);
// merge the isomaps:
merged_isomap = isomap_averaging.add_and_merge(isomap);
cv::imwrite(outputfile.string(), isomap);
}
}
// save the merged isomap:
fs::path outputfile = outputfilebase;
outputfile += fs::path("merged.isomap.png");
cv::imwrite(outputfile.string(), merged_isomap);
outputfile.replace_extension("");
// save the frontal rendering with merged isomap:
Mat frontal_rendering;
glm::mat4 modelview_frontal = glm::mat4( 1.0 );
core::Mesh neutral_expression = morphablemodel::sample_to_mesh(
morphable_model.get_shape_model().draw_sample(pca_shape_coefficients),
morphable_model.get_color_model().get_mean(),
morphable_model.get_shape_model().get_triangle_list(), morphable_model.get_color_model().get_triangle_list(), morphable_model.get_texture_coordinates());
std::tie(frontal_rendering, std::ignore) = render::render(neutral_expression, modelview_frontal, glm::ortho(-130.0f, 130.0f, -130.0f, 130.0f), 512, 512, render::create_mipmapped_texture(merged_isomap), true, false, false);
outputfile.replace_extension(".frontal.png");
cv::imwrite(outputfile.string(), frontal_rendering);
outputfile.replace_extension("");
// Save the mesh as textured obj:
std::string obj_filename = "_" + std::to_string(n_iter) + ".obj";
outputfile.replace_extension(obj_filename.c_str());
core::write_textured_obj(morphable_model.draw_sample(
pca_shape_coefficients, std::vector<float>()), outputfile.string());
std::cout << "Finished fitting and wrote result mesh and isomap to files with basename " << outputfilebase << "." << std::endl;
}
/**
* This app demonstrates estimation of the camera and fitting of the shape
* model of a 3D Morphable Model from an ibug LFPW image with its landmarks.
......@@ -92,6 +207,9 @@ int main(int argc, char *argv[]) {
fs::path modelfile, isomapfile, videofile, landmarksfile, mappingsfile, contourfile, edgetopologyfile, blendshapesfile, outputfile;
std::vector<std::string> annotations;
// get annotaitions from one file
bool get_annotations = false;
try {
po::options_description desc("Allowed options");
desc.add_options()
......@@ -101,7 +219,9 @@ int main(int argc, char *argv[]) {
"a Morphable Model stored as cereal BinaryArchive")
("video,i", po::value<fs::path>(&videofile)->required(),
"an input image")
("annotations,l", po::value<vector<std::string>>(&annotations)->multitoken(),
("get_annotations,g", po::bool_switch(&get_annotations)->default_value(false),
"read .pts annotation file locations from one file, one file path per line")
("annotations,l", po::value<vector<std::string>>(&annotations)->multitoken()->default_value("../bin/data/vid_annot_pts_files.txt"),
".pts annotation files per frame of video")
("mapping,p", po::value<fs::path>(&mappingsfile)->required()->default_value("../share/ibug2did.txt"),
"2D landmarks for the image, in ibug .pts format")
......@@ -140,10 +260,13 @@ int main(int argc, char *argv[]) {
return EXIT_FAILURE;
}
// Load landmarks, LandmarkMapper and the Morphable Model:
core::LandmarkMapper landmark_mapper = core::LandmarkMapper(mappingsfile);
// load all annotation files into lists of landmarks
vector<core::LandmarkCollection<cv::Vec2f>> landmark_list;
try {
landmark_list = eos::core::load_annotations<cv::Vec2f, cv::Vec3f>(annotations, mappingsfile);
std::tie(landmark_list, annotations) = eos::core::load_annotations<cv::Vec2f>(annotations, landmark_mapper, morphable_model, get_annotations);
} catch(const std::runtime_error &e) {
std::cout << e.what() << std::endl;
return EXIT_FAILURE;
......@@ -159,20 +282,14 @@ int main(int argc, char *argv[]) {
// The edge topology is used to speed up computation of the occluding face contour fitting:
morphablemodel::EdgeTopology edge_topology = morphablemodel::load_edge_topology(edgetopologyfile.string());
// Load landmarks, LandmarkMapper and the Morphable Model:
LandmarkCollection <cv::Vec2f> landmarks;
core::LandmarkMapper landmark_mapper = core::LandmarkMapper(mappingsfile);
// These will be the final 2D and 3D points used for the fitting:
vector<cv::Vec3f> model_points; // the points in the 3D shape model
vector<int> vertex_indices; // their vertex indices
vector<core::Mesh> meshs;
vector<fitting::RenderingParameters> rendering_paramss;
// std::tie(model_points, vertex_indices) = eos::core::load_model_data<cv::Vec2f, cv::Vec3f>(landmarks, morphable_model, landmark_mapper);
BufferedVideoIterator vid_iterator;
try {
vid_iterator = BufferedVideoIterator(videofile.string());
} catch(std::runtime_error &e) {
......@@ -185,28 +302,27 @@ int main(int argc, char *argv[]) {
int frame_height = vid_iterator.height;
// test with loading 10 frames subsequently.
std::deque<eos::video::Keyframe> frames = vid_iterator.next();
std::deque<eos::video::Keyframe> key_frames = vid_iterator.next();
std::vector<float> pca_shape_coefficients;
std::vector<std::vector<float>> blend_shape_coefficients;
std::vector<std::vector<cv::Vec2f>> fitted_image_points;
int n_frames = static_cast<int>(frames.size());
while(!(frames.empty())) {
if (n_frames == 100) {
int n_iter = 0;
while(!(key_frames.empty())) {
if (n_iter == 10) {
break;
}
int frame_count = 0;
std::cout << std::endl << "frames in buffer: " << frames.size() << std::endl;
std::tie(meshs, rendering_paramss) = fitting::fit_shape_and_pose_multi(
morphable_model,
blend_shapes,
landmark_list,
landmark_mapper,
frame_width,
frame_height,
static_cast<int>(frames.size()),
key_frames[0].frame.cols,
key_frames[0].frame.rows,
static_cast<int>(key_frames.size()),
edge_topology,
ibug_contour,
model_contour,
......@@ -219,15 +335,30 @@ int main(int argc, char *argv[]) {
fitted_image_points
);
frames = vid_iterator.next();
evaluate_results(
key_frames,
rendering_paramss,
landmark_list,
morphable_model,
meshs,
pca_shape_coefficients,
blend_shape_coefficients,
fitted_image_points,
annotations,
n_iter
);
key_frames = vid_iterator.next();
n_iter++;
}
return EXIT_SUCCESS;
n_frames++;
}
// // iterator through all frames, not needed persee.
// for (std::deque<eos::video::Keyframe>::iterator it = frames.begin(); it!=frames.end(); ++it) {
// std::cout << it->score << " ";
// frame_count++;
// }
}
}
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