/*
  FaceDetector.cpp
  
  Author: Sergi Diaz Planas (2017)
*/
#include "FaceDetector.h"

// Constructor
FaceDetector::FaceDetector()
{
    this->classifierLoaded = false;
    this->predictorLoaded  = false;
}

// Constructor, files containing cascade classifier and shape predictor
// passed as arguments.
FaceDetector::FaceDetector(std::string classifierFile,
                           std::string predictorFile)
{
    this->classifierLoaded = loadClassifier(classifierFile);
    this->predictorLoaded  = loadShapePredictor(predictorFile);
}

// Try to load cascade classifier from file located at input path. Return
// true if successful.
bool FaceDetector::loadClassifier(std::string file) {
    this->classifierLoaded = this->classifier.load(file);
    return this->classifierLoaded;
}

// Try to load shape predictor from file located at input path. Return
// true if successful.
bool FaceDetector::loadShapePredictor(std::string file)
{
    try
    {
        deserialize(file) >> this->shapePredictor;
        this->predictorLoaded = true;
        return true;
    }
    catch (int e)
    {
        this->predictorLoaded = false;
        return false;
    }
}

// Return true if both classifier and shape predictor have
// been loaded.
bool FaceDetector::isLoaded()
{
    return this->predictorLoaded && this->classifierLoaded;
}

// Return a Face image of the face found in given opencv image.
// The method assumes that only 1 face is to be found. If no face
// can be found, it returns the input image.
Face FaceDetector::getFace(Image image)
{
    // Return an empty face if classifier or shape predictor
    // have not been loaded
    Face face = Face();
    if (!this->isLoaded())
        return face;

    Mat mat = image.getImage();

    // Turn image to grayscale if necessary
    Mat mat_gray;
    if (mat.channels() == 3)
        cvtColor(mat, mat_gray, COLOR_BGR2GRAY);
    else if (mat.channels() == 4)
	cvtColor(mat, mat_gray, COLOR_BGRA2GRAY);
    else
        mat_gray = mat.clone();

    // Return an empty face if image is empty
    if (mat_gray.empty())
        return face;

    // Search for faces in given image
    std::vector<Rect> rects = getRectFaces(mat_gray);

    // Discard those regions that do no contain features. This step
    // is used to discard false positives in face detection.
    for (unsigned int i=0; i<rects.size(); i++) {

        full_object_detection features = getFacialFeatures(mat_gray, rects[i]);

        // Check if current face holds the necessary features: left eye, mouth, right eye
        if (features.num_parts() >= 5) {

            // Obtain a rotated chip of face, and store it in the Face object with the (x,y)
            // positions for left and right eye.
            dlib::array2d<unsigned char> face_chip = getFaceChip(mat_gray, features);
            face.setImage(dlib::toMat(face_chip));

            // For the eyes, we are saving the extremal points of both eyes (the ones nearer to
            // the ears), those are features[0] and features[4].
            std::vector<cv::Point> eyes;
            eyes.push_back(cv::Point(features.part(0)(0), features.part(0)(1)));
            eyes.push_back(cv::Point(features.part(4)(0), features.part(4)(1)));

            // Again, we are assuming that only one face is to be found.
            return face;
        }
    }
    // Return empty face if no face with minimum features could be found.
    return face;
}

// Return a collection of opencv rectangles where the faces in given
// image are to be found.
std::vector<Rect> FaceDetector::getRectFaces(Mat image)
{
    // Equalize histogram and perform detection by cascade classifier
    // using haar features.
    //equalizeHist(image, image);
    std::vector<Rect> faces;
    this->classifier.detectMultiScale(image, faces,
                                      1.1, 2, 0|cv::CASCADE_SCALE_IMAGE,
                                      cv::Size(30,30));
    return faces;
}

// Return a collecion of dlib-type features found in the given image regions
// delimited by the given opencv rectangles.
// When performing correctly, a 5-size vector should be returned, where positions
// [0,1] belong to left eye, [2] to mouth and [3,4] to right eye.
full_object_detection FaceDetector::getFacialFeatures(Mat cvImage, Rect cvDet)
{
    // Convert from cv::Rect to dlib::rectangle
    dlib::rectangle dlibDet = openCVRectToDlib(cvDet);
    // Turn cv::Mat into something dlib can deal with
    dlib::cv_image<unsigned char> image(cvImage);

    // Extract features
    full_object_detection features = this->shapePredictor(image, dlibDet);
    return features;
}

// Extract a face chip from image using given features. Eyes positions are used to rotate the
// face image.
dlib::array2d<unsigned char>FaceDetector::getFaceChip(Mat cvImage, full_object_detection feat)
{
    dlib::array2d<unsigned char> face_chip;
    // Turn cv::Mat into something dlib can deal with
    dlib::cv_image<unsigned char> image(cvImage);
    // Get a rotated copy of the face image
    dlib::extract_image_chip(image,
                             dlib::get_face_chip_details(feat),
                             face_chip);
    return face_chip;
}
